mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
fix(oidc): add preferred username claim (#2801)
This adds the missing preferred username claim to the ID Token for OIDC. Fixes #2798
This commit is contained in:
parent
e4391892b5
commit
06641cd15a
|
@ -19,8 +19,8 @@ providers for authentication and authorization. We do not intend to support this
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
We have decided to implement [OpenID Connect] as a beta feature, it's suggested you only utilize it for testing and
|
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
|
providing feedback, and should take caution in relying on it in production as of now. [OpenID Connect] and it's related
|
||||||
are not enabled by default unless you specifically configure the [OpenID Connect] section.
|
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
|
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
|
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
|
### openid
|
||||||
|
|
||||||
This is the default scope for openid. This field is forced on every client by the configuration
|
This is the default scope for openid. This field is forced on every client by the configuration validation that Authelia
|
||||||
validation that Authelia does.
|
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 |
|
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||||
|:-------:|:-----------:|:----------------:|:-------------------------------------------:|
|
|:------------------:|:-------------:|:------------------:|:---------------------------------------------:|
|
||||||
| sub | string | Username | The username the user used to login with |
|
| sub | string | Username | The username the user used to login with |
|
||||||
| scope | string | scopes | Granted scopes (space delimited) |
|
| scope | string | scopes | Granted scopes (space delimited) |
|
||||||
| scp | array[string] | scopes | Granted scopes |
|
| 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 |
|
| rat | number | _N/A_ | The time when the token was requested |
|
||||||
| iat | number | _N/A_ | The time when the token was issued |
|
| iat | number | _N/A_ | The time when the token was issued |
|
||||||
| jti | string(uuid) | _N/A_ | JWT Identifier |
|
| jti | string(uuid) | _N/A_ | JWT Identifier |
|
||||||
|
| preferred_username | string | Username | The username the user used to login with |
|
||||||
|
|
||||||
### groups
|
### groups
|
||||||
|
|
||||||
This scope includes the groups the authentication backend reports the user is a member of in the token.
|
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 |
|
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||||
|:-------:|:-----------:|:----------------:|:--------------------:|
|
|:---------:|:-------------:|:------------------:|:----------------------:|
|
||||||
| groups | array[string] | Groups | The users display name |
|
| groups | array[string] | Groups | The users display name |
|
||||||
|
|
||||||
### email
|
### 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.
|
This scope includes the email information the authentication backend reports about the user in the token.
|
||||||
|
|
||||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||||
|:------------:|:-----------:|:----------------:|:-------------------------------------------------------:|
|
|:--------------:|:-------------:|:------------------:|:---------------------------------------------------------:|
|
||||||
| email | string | email[0] | The first email address in the list of emails |
|
| 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 |
|
| 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 |
|
| 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.
|
This scope includes the profile information the authentication backend reports about the user in the token.
|
||||||
|
|
||||||
| JWT Field | JWT Type | Authelia Attribute | Description |
|
| JWT Field | JWT Type | Authelia Attribute | Description |
|
||||||
|:-------:|:------:|:----------------:|:--------------------:|
|
|:---------:|:--------:|:------------------:|:----------------------:|
|
||||||
| name | string | display_name | The users display name |
|
| name | string | display_name | The users display name |
|
||||||
|
|
||||||
## Endpoint Implementations
|
## 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.
|
Authelia via https://auth.example.com, the discovery URL is https://auth.example.com/.well-known/openid-configuration.
|
||||||
|
|
||||||
| Endpoint | Path |
|
| Endpoint | Path |
|
||||||
|:-----------:|:------------------------------:|
|
|:-------------:|:--------------------------------:|
|
||||||
| Discovery | .well-known/openid-configuration |
|
| Discovery | .well-known/openid-configuration |
|
||||||
| JWKS | api/oidc/jwks |
|
| JWKS | api/oidc/jwks |
|
||||||
| Authorization | api/oidc/authorize |
|
| Authorization | api/oidc/authorize |
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
"github.com/authelia/authelia/v4/internal/oidc"
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"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) {
|
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(),
|
"kid": ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
||||||
}},
|
}},
|
||||||
Subject: userSession.Username,
|
Subject: userSession.Username,
|
||||||
|
Username: userSession.Username,
|
||||||
},
|
},
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
})
|
})
|
||||||
|
@ -107,40 +107,6 @@ func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, ar, response)
|
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(
|
func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||||
ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
|
ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
|
||||||
rw http.ResponseWriter, r *http.Request,
|
rw http.ResponseWriter, r *http.Request,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/fosite/handler/openid"
|
"github.com/ory/fosite/handler/openid"
|
||||||
"github.com/ory/fosite/token/jwt"
|
"github.com/ory/fosite/token/jwt"
|
||||||
|
|
||||||
|
@ -30,3 +31,43 @@ func newOpenIDSession(subject string) *oidc.OpenIDSession {
|
||||||
Extra: map[string]interface{}{},
|
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"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,3 +33,107 @@ func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
||||||
requestedAudience = []string{"https://not.authelia.com"}
|
requestedAudience = []string{"https://not.authelia.com"}
|
||||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
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{}
|
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
Block a user