Finish implementation of requested claim access
This commit is contained in:
parent
744440ee54
commit
8b0f497f5a
8 changed files with 389 additions and 182 deletions
|
@ -6,6 +6,10 @@ other = "Du hast ein gültiges Client-Zertifikat für die folgenden E-Mail-Adres
|
||||||
hash = "sha1-c1ad2600848ad6293ae6df6a04b0b2318bb303f9"
|
hash = "sha1-c1ad2600848ad6293ae6df6a04b0b2318bb303f9"
|
||||||
other = "Willst du dieses Zertifikat für die Anmeldung verwenden oder möchtest du lieber ein anderes Verfahren nutzen?"
|
other = "Willst du dieses Zertifikat für die Anmeldung verwenden oder möchtest du lieber ein anderes Verfahren nutzen?"
|
||||||
|
|
||||||
|
[ClaimsInformation]
|
||||||
|
hash = "sha1-4a6721995b5d87c02be77695910af642ca30b18a"
|
||||||
|
other = "Zusätzlich möchte die Anwendung Zugriff auf folgende Informationen:"
|
||||||
|
|
||||||
[ErrorEmail]
|
[ErrorEmail]
|
||||||
hash = "sha1-d2306dd8970ff616631a3501791297f31475e416"
|
hash = "sha1-d2306dd8970ff616631a3501791297f31475e416"
|
||||||
other = "Bitte gib eine gültige E-Mailadresse ein."
|
other = "Bitte gib eine gültige E-Mailadresse ein."
|
||||||
|
@ -57,8 +61,8 @@ hash = "sha1-f58b8378238bd433deef3c3e6b0b70d0fd0dd59e"
|
||||||
other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du mehr Informationen zu <strong>{{ .client }}</strong>."
|
other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du mehr Informationen zu <strong>{{ .client }}</strong>."
|
||||||
|
|
||||||
[IntroConsentRequested]
|
[IntroConsentRequested]
|
||||||
hash = "sha1-cb8efc74b5b726201321e0924747bf38d39629a1"
|
hash = "sha1-3ac6a3583d40b5e8930c57531f0be9706f1e0194"
|
||||||
other = "Die Anwendung <strong>{{ .client }}</strong> benötigt deine Einwilligungung für die angefragten Berechtigungen."
|
other = "Die Anwendung <strong>{{ .client }}</strong> hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
|
||||||
|
|
||||||
[LabelAcceptCertLogin]
|
[LabelAcceptCertLogin]
|
||||||
description = "Label for a button to accept certificate login"
|
description = "Label for a button to accept certificate login"
|
||||||
|
@ -96,9 +100,9 @@ other = "Ausloggen"
|
||||||
hash = "sha1-e50e5ea384cad8fac6f918d698be373b1362b351"
|
hash = "sha1-e50e5ea384cad8fac6f918d698be373b1362b351"
|
||||||
other = "Zugriff auf deine primäre E-Mail-Adresse."
|
other = "Zugriff auf deine primäre E-Mail-Adresse."
|
||||||
|
|
||||||
[Scope-offline_access-Description]
|
[Scope-offline-Description]
|
||||||
hash = "sha1-732881bf998daa62cbad8615b2e6feb7a053b123"
|
hash = "sha1-732881bf998daa62cbad8615b2e6feb7a053b123"
|
||||||
other = "Zugriff auf deine Informationen behalten, bis du diese Zustimmung widerrufst."
|
other = "Zugriff auf deine Daten behalten, bis du diese Berechtigung widerrufst."
|
||||||
|
|
||||||
[Scope-openid-Description]
|
[Scope-openid-Description]
|
||||||
hash = "sha1-0ad714e7a22b97d8247b70254990256bffa2ef76"
|
hash = "sha1-0ad714e7a22b97d8247b70254990256bffa2ef76"
|
||||||
|
@ -115,3 +119,7 @@ other = "Anwendung erbittet deine Zustimmung"
|
||||||
[WrongOrLockedUserOrInvalidPassword]
|
[WrongOrLockedUserOrInvalidPassword]
|
||||||
hash = "sha1-87e0a0ac67c6c3a06bed184e10b22aae4d075b64"
|
hash = "sha1-87e0a0ac67c6c3a06bed184e10b22aae4d075b64"
|
||||||
other = "Du hast einen ungültigen Nutzernamen oder ein ungültiges Passwort eingegeben oder dein Benutzerkonto wurde gesperrt."
|
other = "Du hast einen ungültigen Nutzernamen oder ein ungültiges Passwort eingegeben oder dein Benutzerkonto wurde gesperrt."
|
||||||
|
|
||||||
|
[claim-CAcert-groups-description]
|
||||||
|
hash = "sha1-62e8788623838cfe2185e315d3a979cbb2eea3b5"
|
||||||
|
other = "Deine CAcert-Team- oder Gruppenzugehörigkeiten."
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
CertLoginIntroText = "You have presented a valid client certificate for the following email addresses:"
|
CertLoginIntroText = "You have presented a valid client certificate for the following email addresses:"
|
||||||
CertLoginRequestText = "Do you want to use this certificate for authentication or do you want to use a different method?"
|
CertLoginRequestText = "Do you want to use this certificate for authentication or do you want to use a different method?"
|
||||||
|
ClaimsInformation = "In addition the application wants access to the following information:"
|
||||||
ErrorEmail = "Please enter a valid email address."
|
ErrorEmail = "Please enter a valid email address."
|
||||||
ErrorEmailRequired = "Please enter an email address."
|
ErrorEmailRequired = "Please enter an email address."
|
||||||
ErrorPasswordRequired = "Please enter a password."
|
ErrorPasswordRequired = "Please enter a password."
|
||||||
|
@ -10,16 +11,17 @@ IndexGreeting = "Hello {{ .User }}"
|
||||||
IndexIntroductionText = "This is an authorization protected resource"
|
IndexIntroductionText = "This is an authorization protected resource"
|
||||||
IndexTitle = "Welcome to the Demo application"
|
IndexTitle = "Welcome to the Demo application"
|
||||||
IntroConsentMoreInformation = "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>."
|
IntroConsentMoreInformation = "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>."
|
||||||
IntroConsentRequested = "The <strong>{{ .client }}</strong> application wants your consent for the requested set of permissions."
|
IntroConsentRequested = "The <strong>{{ .client }}</strong> application requested your consent for the following set of permissions:"
|
||||||
LabelConsent = "I hereby agree that the application may get the requested permissions."
|
LabelConsent = "I hereby agree that the application may get the requested permissions."
|
||||||
LabelSubmit = "Submit"
|
LabelSubmit = "Submit"
|
||||||
LoginTitle = "Login"
|
LoginTitle = "Login"
|
||||||
Scope-email-Description = "Access your primary email address."
|
Scope-email-Description = "Access your primary email address."
|
||||||
Scope-offline_access-Description = "Keep access to your information until you revoke the permission."
|
Scope-offline-Description = "Keep access to your information until you revoke the permission."
|
||||||
Scope-openid-Description = "Request information about your identity."
|
Scope-openid-Description = "Request information about your identity."
|
||||||
Scope-profile-Description = "Access your user profile information including your name, birth date and locale."
|
Scope-profile-Description = "Access your user profile information including your name, birth date and locale."
|
||||||
TitleRequestConsent = "Application requests your consent"
|
TitleRequestConsent = "Application requests your consent"
|
||||||
WrongOrLockedUserOrInvalidPassword = "You entered an invalid username or password or your account has been locked."
|
WrongOrLockedUserOrInvalidPassword = "You entered an invalid username or password or your account has been locked."
|
||||||
|
claim-CAcert-groups-description = "Your CAcert team or group assignments."
|
||||||
|
|
||||||
[FormLabelEmail]
|
[FormLabelEmail]
|
||||||
description = "Label for an email form field"
|
description = "Label for an email form field"
|
||||||
|
|
|
@ -147,12 +147,20 @@ func (i IndividualClaimRequest) AllowedValues() []string {
|
||||||
// OpenIDConfiguration contains the parts of the OpenID discovery information
|
// OpenIDConfiguration contains the parts of the OpenID discovery information
|
||||||
// that are relevant for us.
|
// that are relevant for us.
|
||||||
//
|
//
|
||||||
// Specification
|
// Specifications
|
||||||
//
|
//
|
||||||
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||||
|
//
|
||||||
|
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata
|
||||||
type OpenIDConfiguration struct {
|
type OpenIDConfiguration struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
TokenEndpoint string `json:"token_endpoint"`
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
UserInfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
JwksUri string `json:"jwks_uri"`
|
JwksUri string `json:"jwks_uri"`
|
||||||
|
RegistrationEndpoint string `json:"registration_endpoint"`
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint"`
|
EndSessionEndpoint string `json:"end_session_endpoint"`
|
||||||
|
ClaimTypesSupported []string `json:"claim_types_supported"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,16 @@ func (m *MessageCatalog) LookupMessage(id string, templateData map[string]interf
|
||||||
TemplateData: templateData,
|
TemplateData: templateData,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case *i18n.MessageNotFoundErr:
|
||||||
|
m.logger.Warnf("message %s not found: %v", id, err)
|
||||||
|
if translation != "" {
|
||||||
|
return translation
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
m.logger.Error(err)
|
m.logger.Error(err)
|
||||||
|
}
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
return translation
|
return translation
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -22,7 +21,7 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"git.cacert.org/oidc_login/common/handlers"
|
"git.cacert.org/oidc_login/common/handlers"
|
||||||
models2 "git.cacert.org/oidc_login/common/models"
|
commonModels "git.cacert.org/oidc_login/common/models"
|
||||||
commonServices "git.cacert.org/oidc_login/common/services"
|
commonServices "git.cacert.org/oidc_login/common/services"
|
||||||
"git.cacert.org/oidc_login/idp/services"
|
"git.cacert.org/oidc_login/idp/services"
|
||||||
)
|
)
|
||||||
|
@ -37,6 +36,8 @@ type consentHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConsentInformation struct {
|
type ConsentInformation struct {
|
||||||
|
GrantedScopes []string `form:"scope"`
|
||||||
|
SelectedClaims []string `form:"claims"`
|
||||||
ConsentChecked bool `form:"consent"`
|
ConsentChecked bool `form:"consent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +52,55 @@ type UserInfo struct {
|
||||||
Modified mysql.NullTime `db:"modified"`
|
Modified mysql.NullTime `db:"modified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supportedScopes, supportedClaims map[string]*i18n.Message
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeOpenID = "openid"
|
||||||
|
ScopeOffline = "offline"
|
||||||
|
ScopeOfflineAccess = "offline_access"
|
||||||
|
ScopeProfile = "profile"
|
||||||
|
ScopeEmail = "email"
|
||||||
|
|
||||||
|
ClaimCAcertGroups = "https://cacert.localhost/groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
supportedScopes = make(map[string]*i18n.Message)
|
||||||
|
supportedScopes[ScopeOpenID] = &i18n.Message{
|
||||||
|
ID: "Scope-openid-Description",
|
||||||
|
Other: "Request information about your identity.",
|
||||||
|
}
|
||||||
|
supportedScopes[ScopeOffline] = &i18n.Message{
|
||||||
|
ID: "Scope-offline-Description",
|
||||||
|
Other: "Keep access to your information until you revoke the permission.",
|
||||||
|
}
|
||||||
|
supportedScopes[ScopeOfflineAccess] = supportedScopes[ScopeOffline]
|
||||||
|
supportedScopes[ScopeProfile] = &i18n.Message{
|
||||||
|
ID: "Scope-profile-Description",
|
||||||
|
Other: "Access your user profile information including your name, birth date and locale.",
|
||||||
|
}
|
||||||
|
supportedScopes[ScopeEmail] = &i18n.Message{
|
||||||
|
ID: "Scope-email-Description",
|
||||||
|
Other: "Access your primary email address.",
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedClaims = make(map[string]*i18n.Message)
|
||||||
|
supportedClaims[openid.SubjectKey] = nil
|
||||||
|
supportedClaims[openid.EmailKey] = nil
|
||||||
|
supportedClaims[openid.EmailVerifiedKey] = nil
|
||||||
|
supportedClaims[openid.GivenNameKey] = nil
|
||||||
|
supportedClaims[openid.FamilyNameKey] = nil
|
||||||
|
supportedClaims[openid.MiddleNameKey] = nil
|
||||||
|
supportedClaims[openid.NameKey] = nil
|
||||||
|
supportedClaims[openid.BirthdateKey] = nil
|
||||||
|
supportedClaims[openid.ZoneinfoKey] = nil
|
||||||
|
supportedClaims[openid.LocaleKey] = nil
|
||||||
|
supportedClaims[ClaimCAcertGroups] = &i18n.Message{
|
||||||
|
ID: "claim-CAcert-groups-description",
|
||||||
|
Other: "Your CAcert team or group assignments.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (i *UserInfo) GetFullName() string {
|
func (i *UserInfo) GetFullName() string {
|
||||||
nameParts := make([]string, 0)
|
nameParts := make([]string, 0)
|
||||||
if len(i.GivenName) > 0 {
|
if len(i.GivenName) > 0 {
|
||||||
|
@ -72,6 +122,78 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
localizer := i18n.NewLocalizer(h.bundle, accept)
|
localizer := i18n.NewLocalizer(h.bundle, accept)
|
||||||
|
|
||||||
// retrieve consent information
|
// retrieve consent information
|
||||||
|
consentData, requestedClaims, err := h.getRequestedConsentInformation(challenge, r)
|
||||||
|
if err != nil {
|
||||||
|
// error is already handled in getRequestConsentInformation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.renderConsentForm(w, r, consentData, requestedClaims, err, localizer)
|
||||||
|
break
|
||||||
|
case http.MethodPost:
|
||||||
|
var consentInfo ConsentInformation
|
||||||
|
|
||||||
|
// validate input
|
||||||
|
decoder := form.NewDecoder()
|
||||||
|
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
|
||||||
|
h.logger.Error(err)
|
||||||
|
http.Error(
|
||||||
|
w,
|
||||||
|
http.StatusText(http.StatusInternalServerError),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if consentInfo.ConsentChecked {
|
||||||
|
sessionData, err := h.getSessionData(consentInfo, requestedClaims, consentData.Payload, r.Context())
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("could not get session data: %v", err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
consentRequest, err := h.adminClient.AcceptConsentRequest(
|
||||||
|
admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody(
|
||||||
|
&models.AcceptConsentRequest{
|
||||||
|
GrantAccessTokenAudience: nil,
|
||||||
|
GrantScope: consentInfo.GrantedScopes,
|
||||||
|
HandledAt: models.NullTime(time.Now()),
|
||||||
|
Remember: true,
|
||||||
|
RememberFor: 86400,
|
||||||
|
Session: sessionData,
|
||||||
|
}).WithTimeout(time.Second * 10))
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error(err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
return
|
||||||
|
|
||||||
|
} else {
|
||||||
|
consentRequest, err := h.adminClient.RejectConsentRequest(
|
||||||
|
admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody(
|
||||||
|
&models.RejectRequest{}))
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error(err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *consentHandler) getRequestedConsentInformation(challenge string, r *http.Request) (
|
||||||
|
*admin.GetConsentRequestOK,
|
||||||
|
*commonModels.OIDCClaimsRequest,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
consentData, err := h.adminClient.GetConsentRequest(
|
consentData, err := h.adminClient.GetConsentRequest(
|
||||||
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
|
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -94,137 +216,34 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handlers.GetErrorBucket(r).AddError(errorDetails)
|
handlers.GetErrorBucket(r).AddError(errorDetails)
|
||||||
return
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
var requestedClaims commonModels.OIDCClaimsRequest
|
||||||
switch r.Method {
|
requestUrl, err := url.Parse(consentData.Payload.RequestURL)
|
||||||
case http.MethodGet:
|
|
||||||
h.renderConsentForm(w, r, consentData, err, localizer)
|
|
||||||
break
|
|
||||||
case http.MethodPost:
|
|
||||||
var consentInfo ConsentInformation
|
|
||||||
|
|
||||||
// validate input
|
|
||||||
decoder := form.NewDecoder()
|
|
||||||
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
|
|
||||||
h.logger.Error(err)
|
|
||||||
http.Error(
|
|
||||||
w,
|
|
||||||
http.StatusText(http.StatusInternalServerError),
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if consentInfo.ConsentChecked {
|
|
||||||
idTokenData := make(map[string]interface{}, 0)
|
|
||||||
|
|
||||||
db := services.GetDb(h.context)
|
|
||||||
|
|
||||||
stmt, err := db.PreparexContext(
|
|
||||||
r.Context(),
|
|
||||||
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
|
||||||
FROM users
|
|
||||||
WHERE uniqueid = ?
|
|
||||||
AND locked = 0`,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Errorf("error preparing user information SQL: %v", err)
|
h.logger.Warnf("could not parse original request URL %s: %v", consentData.Payload.RequestURL, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() { _ = stmt.Close() }()
|
|
||||||
|
|
||||||
userInfo := &UserInfo{}
|
|
||||||
|
|
||||||
err = stmt.QueryRowxContext(r.Context(), consentData.GetPayload().Subject).StructScan(userInfo)
|
|
||||||
switch {
|
|
||||||
case err == sql.ErrNoRows:
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
case err != nil:
|
|
||||||
h.logger.Errorf("error performing user information SQL: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
for _, scope := range consentData.GetPayload().RequestedScope {
|
|
||||||
switch scope {
|
|
||||||
case "email":
|
|
||||||
// email
|
|
||||||
// OPTIONAL. This scope value requests access to the email and email_verified Claims.
|
|
||||||
idTokenData[openid.EmailKey] = userInfo.Email
|
|
||||||
idTokenData[openid.EmailVerifiedKey] = userInfo.EmailVerified
|
|
||||||
break
|
|
||||||
case "profile":
|
|
||||||
// profile
|
|
||||||
// OPTIONAL. This scope value requests access to the End-User's default profile Claims, which
|
|
||||||
// are: name, family_name, given_name, middle_name, nickname, preferred_username, profile,
|
|
||||||
// picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
|
|
||||||
idTokenData[openid.GivenNameKey] = userInfo.GivenName
|
|
||||||
idTokenData[openid.FamilyNameKey] = userInfo.FamilyName
|
|
||||||
idTokenData[openid.MiddleNameKey] = userInfo.MiddleName
|
|
||||||
idTokenData[openid.NameKey] = userInfo.GetFullName()
|
|
||||||
if userInfo.BirthDate.Valid {
|
|
||||||
idTokenData[openid.BirthdateKey] = userInfo.BirthDate.Time.Format("2006-01-02")
|
|
||||||
}
|
|
||||||
idTokenData[openid.LocaleKey] = userInfo.Language
|
|
||||||
idTokenData["https://cacert.localhost/groups"] = []string{"admin", "user"}
|
|
||||||
if userInfo.Modified.Valid {
|
|
||||||
idTokenData[openid.UpdatedAtKey] = userInfo.Modified.Time.Unix()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "address":
|
|
||||||
// address
|
|
||||||
// OPTIONAL. This scope value requests access to the address Claim.
|
|
||||||
break
|
|
||||||
case "phone":
|
|
||||||
// phone
|
|
||||||
// OPTIONAL. This scope value requests access to the phone_number and phone_number_verified Claims.
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sessionData := &models.ConsentRequestSession{
|
|
||||||
AccessToken: nil,
|
|
||||||
IDToken: idTokenData,
|
|
||||||
}
|
|
||||||
consentRequest, err := h.adminClient.AcceptConsentRequest(
|
|
||||||
admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody(
|
|
||||||
&models.AcceptConsentRequest{
|
|
||||||
GrantAccessTokenAudience: nil,
|
|
||||||
GrantScope: consentData.GetPayload().RequestedScope,
|
|
||||||
HandledAt: models.NullTime(time.Now()),
|
|
||||||
Remember: true,
|
|
||||||
RememberFor: 86400,
|
|
||||||
Session: sessionData,
|
|
||||||
}).WithTimeout(time.Second * 10))
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error(err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
|
|
||||||
w.WriteHeader(http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
consentRequest, err := h.adminClient.RejectConsentRequest(
|
claimsParameter := requestUrl.Query().Get("claims")
|
||||||
admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody(
|
if claimsParameter != "" {
|
||||||
&models.RejectRequest{}))
|
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
|
||||||
|
err := decoder.Decode(&requestedClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error(err)
|
h.logger.Warnf(
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
"ignoring claims request parameter %s that could not be decoded: %v",
|
||||||
return
|
claimsParameter,
|
||||||
}
|
err,
|
||||||
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
|
)
|
||||||
w.WriteHeader(http.StatusFound)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return consentData, &requestedClaims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *consentHandler) renderConsentForm(
|
func (h *consentHandler) renderConsentForm(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
consentData *admin.GetConsentRequestOK,
|
consentData *admin.GetConsentRequestOK,
|
||||||
|
claims *commonModels.OIDCClaimsRequest,
|
||||||
err error,
|
err error,
|
||||||
localizer *i18n.Localizer,
|
localizer *i18n.Localizer,
|
||||||
) {
|
) {
|
||||||
|
@ -235,21 +254,6 @@ func (h *consentHandler) renderConsentForm(
|
||||||
return h.messageCatalog.LookupMessage(id, nil, localizer)
|
return h.messageCatalog.LookupMessage(id, nil, localizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestedClaims models2.OIDCClaimsRequest
|
|
||||||
requestUrl, err := url.Parse(consentData.Payload.RequestURL)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Warnf("could not parse original request URL %s: %v", consentData.Payload.RequestURL, err)
|
|
||||||
} else {
|
|
||||||
claimsParameter := requestUrl.Query().Get("claims")
|
|
||||||
if claimsParameter != "" {
|
|
||||||
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
|
|
||||||
err := decoder.Decode(&requestedClaims)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Warnf("ignoring claims request parameter %s that could not be decoded: %v", claimsParameter, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render consent form
|
// render consent form
|
||||||
client := consentData.GetPayload().Client
|
client := consentData.GetPayload().Client
|
||||||
err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
||||||
|
@ -258,12 +262,14 @@ func (h *consentHandler) renderConsentForm(
|
||||||
"errors": map[string]string{},
|
"errors": map[string]string{},
|
||||||
"client": client,
|
"client": client,
|
||||||
"requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer),
|
"requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer),
|
||||||
|
"requestedClaims": h.mapRequestedClaims(claims, localizer),
|
||||||
"LabelSubmit": trans("LabelSubmit"),
|
"LabelSubmit": trans("LabelSubmit"),
|
||||||
"LabelConsent": trans("LabelConsent"),
|
"LabelConsent": trans("LabelConsent"),
|
||||||
"IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{
|
"IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{
|
||||||
"client": client.ClientName,
|
"client": client.ClientName,
|
||||||
"clientLink": client.ClientURI,
|
"clientLink": client.ClientURI,
|
||||||
})),
|
})),
|
||||||
|
"ClaimsInformation": template.HTML(trans("ClaimsInformation", nil)),
|
||||||
"IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{
|
"IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{
|
||||||
"client": client.ClientName,
|
"client": client.ClientName,
|
||||||
})),
|
})),
|
||||||
|
@ -278,12 +284,181 @@ type scopeWithLabel struct {
|
||||||
func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter, localizer *i18n.Localizer) []*scopeWithLabel {
|
func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter, localizer *i18n.Localizer) []*scopeWithLabel {
|
||||||
result := make([]*scopeWithLabel, 0)
|
result := make([]*scopeWithLabel, 0)
|
||||||
for _, scopeName := range scope {
|
for _, scopeName := range scope {
|
||||||
result = append(result, &scopeWithLabel{Name: scopeName, Label: h.messageCatalog.LookupMessage(
|
if _, ok := supportedScopes[scopeName]; !ok {
|
||||||
fmt.Sprintf("Scope-%s-Description", scopeName), nil, localizer)})
|
h.logger.Warnf("unsupported scope %s ignored", scopeName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
label, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
|
DefaultMessage: supportedScopes[scopeName],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warnf("could not localize label for scope %s: %v", scopeName, err)
|
||||||
|
label = scopeName
|
||||||
|
}
|
||||||
|
result = append(result, &scopeWithLabel{Name: scopeName, Label: label})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type claimWithLabel struct {
|
||||||
|
Name string
|
||||||
|
Label string
|
||||||
|
Essential bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *consentHandler) mapRequestedClaims(claims *commonModels.OIDCClaimsRequest, localizer *i18n.Localizer) []*claimWithLabel {
|
||||||
|
result := make([]*claimWithLabel, 0)
|
||||||
|
known := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, claimElement := range []*commonModels.ClaimElement{claims.GetUserInfo(), claims.GetIDToken()} {
|
||||||
|
if claimElement != nil {
|
||||||
|
for k, v := range *claimElement {
|
||||||
|
if _, ok := supportedClaims[k]; !ok {
|
||||||
|
h.logger.Warnf("unsupported claim %s ignored", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
label, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
|
DefaultMessage: supportedClaims[k],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warnf("could not localize label for claim %s: %v", k, err)
|
||||||
|
label = k
|
||||||
|
}
|
||||||
|
if !known[k] {
|
||||||
|
result = append(result, &claimWithLabel{
|
||||||
|
Name: k,
|
||||||
|
Label: label,
|
||||||
|
Essential: v.IsEssential(),
|
||||||
|
})
|
||||||
|
known[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *consentHandler) getSessionData(
|
||||||
|
info ConsentInformation,
|
||||||
|
claims *commonModels.OIDCClaimsRequest,
|
||||||
|
payload *models.ConsentRequest,
|
||||||
|
ctx context.Context,
|
||||||
|
) (*models.ConsentRequestSession, error) {
|
||||||
|
idTokenData := make(map[string]interface{}, 0)
|
||||||
|
accessTokenData := make(map[string]interface{}, 0)
|
||||||
|
|
||||||
|
db := services.GetDb(h.context)
|
||||||
|
stmt, err := db.PreparexContext(
|
||||||
|
ctx,
|
||||||
|
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
||||||
|
FROM users
|
||||||
|
WHERE uniqueid = ?
|
||||||
|
AND locked = 0`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("error preparing user information SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = stmt.Close() }()
|
||||||
|
|
||||||
|
userInfo := &UserInfo{}
|
||||||
|
|
||||||
|
err = stmt.QueryRowxContext(ctx, payload.Subject).StructScan(userInfo)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
h.logger.Errorf("could not find entry for subject %s", payload.Subject)
|
||||||
|
return nil, err
|
||||||
|
case err != nil:
|
||||||
|
h.logger.Errorf("error performing user information SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
h.fillTokenData(accessTokenData, payload.RequestedScope, claims, info, userInfo)
|
||||||
|
h.fillTokenData(idTokenData, payload.RequestedScope, claims, info, userInfo)
|
||||||
|
return &models.ConsentRequestSession{
|
||||||
|
AccessToken: accessTokenData,
|
||||||
|
IDToken: idTokenData,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *consentHandler) fillTokenData(m map[string]interface{}, requestedScope models.StringSlicePipeDelimiter, claimsRequest *commonModels.OIDCClaimsRequest, consentInformation ConsentInformation, userInfo *UserInfo) {
|
||||||
|
for _, scope := range requestedScope {
|
||||||
|
granted := false
|
||||||
|
for _, k := range consentInformation.GrantedScopes {
|
||||||
|
if k == scope {
|
||||||
|
granted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !granted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch scope {
|
||||||
|
case ScopeEmail:
|
||||||
|
// email
|
||||||
|
// OPTIONAL. This scope value requests access to the email and
|
||||||
|
// email_verified Claims.
|
||||||
|
m[openid.EmailKey] = userInfo.Email
|
||||||
|
m[openid.EmailVerifiedKey] = userInfo.EmailVerified
|
||||||
|
break
|
||||||
|
case ScopeProfile:
|
||||||
|
// profile
|
||||||
|
// OPTIONAL. This scope value requests access to the
|
||||||
|
// End-User's default profile Claims, which are: name,
|
||||||
|
// family_name, given_name, middle_name, nickname,
|
||||||
|
// preferred_username, profile, picture, website, gender,
|
||||||
|
// birthdate, zoneinfo, locale, and updated_at.
|
||||||
|
m[openid.GivenNameKey] = userInfo.GivenName
|
||||||
|
m[openid.FamilyNameKey] = userInfo.FamilyName
|
||||||
|
m[openid.MiddleNameKey] = userInfo.MiddleName
|
||||||
|
m[openid.NameKey] = userInfo.GetFullName()
|
||||||
|
if userInfo.BirthDate.Valid {
|
||||||
|
m[openid.BirthdateKey] = userInfo.BirthDate.Time.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
m[openid.LocaleKey] = userInfo.Language
|
||||||
|
if userInfo.Modified.Valid {
|
||||||
|
m[openid.UpdatedAtKey] = userInfo.Modified.Time.Unix()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userInfoClaims := claimsRequest.GetUserInfo(); userInfoClaims != nil {
|
||||||
|
for claimName, claim := range *userInfoClaims {
|
||||||
|
granted := false
|
||||||
|
for _, k := range consentInformation.SelectedClaims {
|
||||||
|
if k == claimName {
|
||||||
|
granted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !granted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if claim.WantedValue() != nil {
|
||||||
|
m[claimName] = *claim.WantedValue()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch claimName {
|
||||||
|
case ClaimCAcertGroups:
|
||||||
|
m[claimName] = []string{"admin", "user"}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if claim.IsEssential() {
|
||||||
|
h.logger.Warnf(
|
||||||
|
"handling for essential claim name %s not implemented",
|
||||||
|
claimName,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
h.logger.Warnf(
|
||||||
|
"handling for claim name %s not implemented",
|
||||||
|
claimName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler, error) {
|
func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler, error) {
|
||||||
consentTemplate, err := template.ParseFiles(
|
consentTemplate, err := template.ParseFiles(
|
||||||
"templates/idp/base.gohtml", "templates/idp/consent.gohtml")
|
"templates/idp/base.gohtml", "templates/idp/consent.gohtml")
|
||||||
|
|
|
@ -44,27 +44,15 @@ func AddMessages(ctx context.Context) {
|
||||||
}
|
}
|
||||||
messages["IntroConsentRequested"] = &i18n.Message{
|
messages["IntroConsentRequested"] = &i18n.Message{
|
||||||
ID: "IntroConsentRequested",
|
ID: "IntroConsentRequested",
|
||||||
Other: "The <strong>{{ .client }}</strong> application wants your consent for the requested set of permissions.",
|
Other: "The <strong>{{ .client }}</strong> application requested your consent for the following set of permissions:",
|
||||||
}
|
}
|
||||||
messages["IntroConsentMoreInformation"] = &i18n.Message{
|
messages["IntroConsentMoreInformation"] = &i18n.Message{
|
||||||
ID: "IntroConsentMoreInformation",
|
ID: "IntroConsentMoreInformation",
|
||||||
Other: "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>.",
|
Other: "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>.",
|
||||||
}
|
}
|
||||||
messages["Scope-openid-Description"] = &i18n.Message{
|
messages["ClaimsInformation"] = &i18n.Message{
|
||||||
ID: "Scope-openid-Description",
|
ID: "ClaimsInformation",
|
||||||
Other: "Request information about your identity.",
|
Other: "In addition the application wants access to the following information:",
|
||||||
}
|
|
||||||
messages["Scope-offline_access-Description"] = &i18n.Message{
|
|
||||||
ID: "Scope-offline_access-Description",
|
|
||||||
Other: "Keep access to your information until you revoke the permission.",
|
|
||||||
}
|
|
||||||
messages["Scope-profile-Description"] = &i18n.Message{
|
|
||||||
ID: "Scope-profile-Description",
|
|
||||||
Other: "Access your user profile information including your name, birth date and locale.",
|
|
||||||
}
|
|
||||||
messages["Scope-email-Description"] = &i18n.Message{
|
|
||||||
ID: "Scope-email-Description",
|
|
||||||
Other: "Access your primary email address.",
|
|
||||||
}
|
}
|
||||||
messages["WrongOrLockedUserOrInvalidPassword"] = &i18n.Message{
|
messages["WrongOrLockedUserOrInvalidPassword"] = &i18n.Message{
|
||||||
ID: "WrongOrLockedUserOrInvalidPassword",
|
ID: "WrongOrLockedUserOrInvalidPassword",
|
||||||
|
|
|
@ -2,17 +2,30 @@
|
||||||
<form class="form-consent" method="post">
|
<form class="form-consent" method="post">
|
||||||
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
<img src="/images/CAcert-logo.svg" width="300" height="68" alt="CAcert" class="mb-4">
|
||||||
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
<h1 class="h3 mb-3">{{ .Title }}</h1>
|
||||||
<p class="text-left">{{ .IntroConsentRequested }}</p>
|
|
||||||
{{ if .client.LogoURI }}
|
{{ if .client.LogoURI }}
|
||||||
<p>
|
<p>
|
||||||
<img src="{{ .client.LogoURI }}" alt="{{ .client.ClientName }}"/>
|
<img src="{{ .client.LogoURI }}" alt="{{ .client.ClientName }}"/>
|
||||||
</p>
|
</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<p class="text-left">{{ .IntroConsentRequested }}</p>
|
||||||
<ul class="list-group text-left small mb-3">
|
<ul class="list-group text-left small mb-3">
|
||||||
{{ range .requestedScope }}
|
{{ range $i, $scope := .requestedScope }}
|
||||||
<li class="list-group-item">{{ .Label }}</li>
|
<li class="list-group-item">
|
||||||
|
<input type="hidden" name="scope[{{ $i }}]" value="{{ $scope.Name }}">
|
||||||
|
{{ $scope.Label }}</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{ if .requestedClaims }}
|
||||||
|
<p class="text-left">{{ .ClaimsInformation }}</p>
|
||||||
|
<ul class="list-group text-left small mb-3">
|
||||||
|
{{ range $i, $claim := .requestedClaims }}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<input type="hidden" name="claims[{{ $i }}]" value="{{ $claim.Name }}">
|
||||||
|
{{ $claim.Label }}{{ if $claim.Essential }} *{{ end}}
|
||||||
|
</li>
|
||||||
|
{{ end}}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
<p class="text-left">{{ .IntroMoreInformation }}</p>
|
<p class="text-left">{{ .IntroMoreInformation }}</p>
|
||||||
|
|
||||||
{{ .csrfField }}
|
{{ .csrfField }}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
[ErrorTitle]
|
[ClaimsInformation]
|
||||||
hash = "sha1-736aec25a98f5ec5b71400bb0163f891f509b566"
|
hash = "sha1-4a6721995b5d87c02be77695910af642ca30b18a"
|
||||||
other = "Es ist ein Fehler aufgetreten"
|
other = "Zusätzlich möchte die Anwendung Zugriff auf folgende Informationen:"
|
||||||
|
|
||||||
|
[IntroConsentRequested]
|
||||||
|
hash = "sha1-3ac6a3583d40b5e8930c57531f0be9706f1e0194"
|
||||||
|
other = "Die Anwendung <strong>{{ .client }}</strong> hat deine Zustimmung für die Erteilung der folgenden Berechtigungen angefragt:"
|
||||||
|
|
Reference in a new issue