fix(oidc): add preferred username claim (#2801)
This adds the missing preferred username claim to the ID Token for OIDC. Fixes #2798pull/2805/head^2
parent
e4391892b5
commit
06641cd15a
|
@ -19,8 +19,8 @@ providers for authentication and authorization. We do not intend to support this
|
|||
## Roadmap
|
||||
|
||||
We have decided to implement [OpenID Connect] as a beta feature, it's suggested you only utilize it for testing and
|
||||
providing feedback, and should take caution in relying on it in production as of now. [OpenID Connect] and it's related endpoints
|
||||
are not enabled by default unless you specifically configure the [OpenID Connect] section.
|
||||
providing feedback, and should take caution in relying on it in production as of now. [OpenID Connect] and it's related
|
||||
endpoints are not enabled by default unless you specifically configure the [OpenID Connect] section.
|
||||
|
||||
As [OpenID Connect] is fairly complex (the [OpenID Connect] Provider role especially so) it's intentional that it is
|
||||
both a beta and that the implemented features are part of a thoughtful roadmap. Items that are not immediately obvious
|
||||
|
@ -489,11 +489,14 @@ characters. For Kubernetes, see [this section too](../secrets.md#Kubernetes).
|
|||
|
||||
### openid
|
||||
|
||||
This is the default scope for openid. This field is forced on every client by the configuration
|
||||
validation that Authelia does.
|
||||
This is the default scope for openid. This field is forced on every client by the configuration validation that Authelia
|
||||
does.
|
||||
|
||||
_**Important Note:** The claim `sub` is planned to be changed in the future to a randomly unique value to identify the
|
||||
individual user. Please use the claim `preferred_username` instead._
|
||||
|
||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||
|:-------:|:-----------:|:----------------:|:-------------------------------------------:|
|
||||
|:------------------:|:-------------:|:------------------:|:---------------------------------------------:|
|
||||
| sub | string | Username | The username the user used to login with |
|
||||
| scope | string | scopes | Granted scopes (space delimited) |
|
||||
| scp | array[string] | scopes | Granted scopes |
|
||||
|
@ -505,13 +508,14 @@ validation that Authelia does.
|
|||
| rat | number | _N/A_ | The time when the token was requested |
|
||||
| iat | number | _N/A_ | The time when the token was issued |
|
||||
| jti | string(uuid) | _N/A_ | JWT Identifier |
|
||||
| preferred_username | string | Username | The username the user used to login with |
|
||||
|
||||
### groups
|
||||
|
||||
This scope includes the groups the authentication backend reports the user is a member of in the token.
|
||||
|
||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||
|:-------:|:-----------:|:----------------:|:--------------------:|
|
||||
|:---------:|:-------------:|:------------------:|:----------------------:|
|
||||
| groups | array[string] | Groups | The users display name |
|
||||
|
||||
### email
|
||||
|
@ -519,7 +523,7 @@ This scope includes the groups the authentication backend reports the user is a
|
|||
This scope includes the email information the authentication backend reports about the user in the token.
|
||||
|
||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||
|:------------:|:-----------:|:----------------:|:-------------------------------------------------------:|
|
||||
|:--------------:|:-------------:|:------------------:|:---------------------------------------------------------:|
|
||||
| email | string | email[0] | The first email address in the list of emails |
|
||||
| email_verified | bool | _N/A_ | If the email is verified, assumed true for the time being |
|
||||
| alt_emails | array[string] | email[1:] | All email addresses that are not in the email JWT field |
|
||||
|
@ -529,7 +533,7 @@ This scope includes the email information the authentication backend reports abo
|
|||
This scope includes the profile information the authentication backend reports about the user in the token.
|
||||
|
||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||
|:-------:|:------:|:----------------:|:--------------------:|
|
||||
|:---------:|:--------:|:------------------:|:----------------------:|
|
||||
| name | string | display_name | The users display name |
|
||||
|
||||
## Endpoint Implementations
|
||||
|
@ -540,7 +544,7 @@ appended to the end of the primary URL used to access Authelia. For example in t
|
|||
Authelia via https://auth.example.com, the discovery URL is https://auth.example.com/.well-known/openid-configuration.
|
||||
|
||||
| Endpoint | Path |
|
||||
|:-----------:|:------------------------------:|
|
||||
|:-------------:|:--------------------------------:|
|
||||
| Discovery | .well-known/openid-configuration |
|
||||
| JWKS | api/oidc/jwks |
|
||||
| Authorization | api/oidc/authorize |
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
|
||||
|
@ -94,6 +93,7 @@ func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *
|
|||
"kid": ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
||||
}},
|
||||
Subject: userSession.Username,
|
||||
Username: userSession.Username,
|
||||
},
|
||||
ClientID: clientID,
|
||||
})
|
||||
|
@ -107,40 +107,6 @@ func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *
|
|||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, ar, response)
|
||||
}
|
||||
|
||||
func oidcGrantRequests(ar fosite.AuthorizeRequester, scopes, audiences []string, userSession *session.UserSession) (extraClaims map[string]interface{}) {
|
||||
extraClaims = map[string]interface{}{}
|
||||
|
||||
for _, scope := range scopes {
|
||||
ar.GrantScope(scope)
|
||||
|
||||
switch scope {
|
||||
case "groups":
|
||||
extraClaims["groups"] = userSession.Groups
|
||||
case "profile":
|
||||
extraClaims["name"] = userSession.DisplayName
|
||||
case "email":
|
||||
if len(userSession.Emails) != 0 {
|
||||
extraClaims["email"] = userSession.Emails[0]
|
||||
if len(userSession.Emails) > 1 {
|
||||
extraClaims["alt_emails"] = userSession.Emails[1:]
|
||||
}
|
||||
// TODO (james-d-elliott): actually verify emails and record that information.
|
||||
extraClaims["email_verified"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, audience := range audiences {
|
||||
ar.GrantAudience(audience)
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(ar.GetClient().GetID(), ar.GetGrantedAudience()) {
|
||||
ar.GrantAudience(ar.GetClient().GetID())
|
||||
}
|
||||
|
||||
return extraClaims
|
||||
}
|
||||
|
||||
func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||
ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
|
||||
rw http.ResponseWriter, r *http.Request,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
|
||||
|
@ -30,3 +31,43 @@ func newOpenIDSession(subject string) *oidc.OpenIDSession {
|
|||
Extra: map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func oidcGrantRequests(ar fosite.AuthorizeRequester, scopes, audiences []string, userSession *session.UserSession) (extraClaims map[string]interface{}) {
|
||||
extraClaims = map[string]interface{}{
|
||||
oidc.ClaimPreferredUsername: userSession.Username,
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
if ar != nil {
|
||||
ar.GrantScope(scope)
|
||||
}
|
||||
|
||||
switch scope {
|
||||
case oidc.ScopeGroups:
|
||||
extraClaims[oidc.ClaimGroups] = userSession.Groups
|
||||
case oidc.ScopeProfile:
|
||||
extraClaims[oidc.ClaimDisplayName] = userSession.DisplayName
|
||||
case oidc.ScopeEmail:
|
||||
if len(userSession.Emails) != 0 {
|
||||
extraClaims[oidc.ClaimEmail] = userSession.Emails[0]
|
||||
if len(userSession.Emails) > 1 {
|
||||
extraClaims[oidc.ClaimAltEmails] = userSession.Emails[1:]
|
||||
}
|
||||
// TODO (james-d-elliott): actually verify emails and record that information.
|
||||
extraClaims[oidc.ClaimEmailVerified] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ar != nil {
|
||||
for _, audience := range audiences {
|
||||
ar.GrantAudience(audience)
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(ar.GetClient().GetID(), ar.GetGrantedAudience()) {
|
||||
ar.GrantAudience(ar.GetClient().GetID())
|
||||
}
|
||||
}
|
||||
|
||||
return extraClaims
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
|
@ -31,3 +33,107 @@ func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
|||
requestedAudience = []string{"https://not.authelia.com"}
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeOpenID(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeOpenID}, []string{}, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 1)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername])
|
||||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeOpenIDAndGroups(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeGroups}, []string{}, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimGroups)
|
||||
assert.Len(t, extraClaims[oidc.ClaimGroups], 2)
|
||||
assert.Contains(t, extraClaims[oidc.ClaimGroups], "admin")
|
||||
assert.Contains(t, extraClaims[oidc.ClaimGroups], "dev")
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeGroups}, []string{}, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "fred", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimGroups)
|
||||
assert.Len(t, extraClaims[oidc.ClaimGroups], 1)
|
||||
assert.Contains(t, extraClaims[oidc.ClaimGroups], "dev")
|
||||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeOpenIDAndEmail(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeEmail}, []string{}, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 4)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimEmail)
|
||||
assert.Equal(t, "j.smith@authelia.com", extraClaims[oidc.ClaimEmail])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimAltEmails)
|
||||
assert.Len(t, extraClaims[oidc.ClaimAltEmails], 1)
|
||||
assert.Contains(t, extraClaims[oidc.ClaimAltEmails], "admin@authelia.com")
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimEmailVerified)
|
||||
assert.Equal(t, true, extraClaims[oidc.ClaimEmailVerified])
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeEmail}, []string{}, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 3)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "fred", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimEmail)
|
||||
assert.Equal(t, "f.smith@authelia.com", extraClaims[oidc.ClaimEmail])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimEmailVerified)
|
||||
assert.Equal(t, true, extraClaims[oidc.ClaimEmailVerified])
|
||||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeOpenIDAndProfile(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeProfile}, []string{}, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "john", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimDisplayName)
|
||||
assert.Equal(t, "John Smith", extraClaims[oidc.ClaimDisplayName])
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeProfile}, []string{}, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimPreferredUsername)
|
||||
assert.Equal(t, "fred", extraClaims[oidc.ClaimPreferredUsername])
|
||||
|
||||
require.Contains(t, extraClaims, oidc.ClaimDisplayName)
|
||||
assert.Equal(t, extraClaims[oidc.ClaimDisplayName], "Fred Smith")
|
||||
}
|
||||
|
||||
var (
|
||||
oidcUserSessionJohn = session.UserSession{
|
||||
Username: "john",
|
||||
Groups: []string{"admin", "dev"},
|
||||
DisplayName: "John Smith",
|
||||
Emails: []string{"j.smith@authelia.com", "admin@authelia.com"},
|
||||
}
|
||||
|
||||
oidcUserSessionFred = session.UserSession{
|
||||
Username: "fred",
|
||||
Groups: []string{"dev"},
|
||||
DisplayName: "Fred Smith",
|
||||
Emails: []string{"f.smith@authelia.com"},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -8,3 +8,21 @@ var scopeDescriptions = map[string]string{
|
|||
}
|
||||
|
||||
var audienceDescriptions = map[string]string{}
|
||||
|
||||
// Scope strings.
|
||||
const (
|
||||
ScopeOpenID = "openid"
|
||||
ScopeProfile = "profile"
|
||||
ScopeEmail = "email"
|
||||
ScopeGroups = "groups"
|
||||
)
|
||||
|
||||
// Claim strings.
|
||||
const (
|
||||
ClaimGroups = "groups"
|
||||
ClaimDisplayName = "name"
|
||||
ClaimPreferredUsername = "preferred_username"
|
||||
ClaimEmail = "email"
|
||||
ClaimEmailVerified = "email_verified"
|
||||
ClaimAltEmails = "alt_emails"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue