feat: support additional claims
Signed-off-by: afsu <suaf2020@163.com> Signed-off-by: af su <saf@zjuici.com>
This commit is contained in:
parent
c6355ee402
commit
252d078d8e
|
|
@ -526,6 +526,7 @@ Provider holds all configuration for a single provider
|
|||
| `scope` | _string_ | Scope is the OAuth scope specification |
|
||||
| `allowedGroups` | _[]string_ | AllowedGroups is a list of restrict logins to members of this group |
|
||||
| `code_challenge_method` | _string_ | The code challenge method |
|
||||
| `additionalClaims` | _[]string_ | Additional claims to be obtained from the `id_token`. |
|
||||
| `backendLogoutURL` | _string_ | URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session |
|
||||
|
||||
### ProviderType
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ type Provider struct {
|
|||
// The code challenge method
|
||||
CodeChallengeMethod string `yaml:"code_challenge_method,omitempty"`
|
||||
|
||||
// Additional claims to be obtained from the `id_token`.
|
||||
AdditionalClaims []string `json:"additionalClaims,omitempty"`
|
||||
|
||||
// URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session
|
||||
BackendLogoutURL string `yaml:"backendLogoutURL"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ type SessionState struct {
|
|||
Groups []string `msgpack:"g,omitempty"`
|
||||
PreferredUsername string `msgpack:"pu,omitempty"`
|
||||
|
||||
// Additional claims
|
||||
AdditionalClaims map[string]interface{} `msgpack:"ac,omitempty"`
|
||||
|
||||
// Internal helpers, not serialized
|
||||
Clock func() time.Time `msgpack:"-"` // override for time.Now, for testing
|
||||
Lock Lock `msgpack:"-"`
|
||||
|
|
@ -156,10 +159,34 @@ func (s *SessionState) GetClaim(claim string) []string {
|
|||
case "preferred_username":
|
||||
return []string{s.PreferredUsername}
|
||||
default:
|
||||
return []string{}
|
||||
return s.getAdditionalClaim(claim)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionState) getAdditionalClaim(claim string) []string {
|
||||
if value, ok := s.AdditionalClaims[claim]; ok {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return []string{v}
|
||||
case []string:
|
||||
return v
|
||||
case []interface{}:
|
||||
result := make([]string, len(v))
|
||||
for i, item := range v {
|
||||
if str, ok := item.(string); ok {
|
||||
result[i] = str
|
||||
} else {
|
||||
result[i] = fmt.Sprintf("%v", item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return []string{fmt.Sprintf("%v", value)}
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckNonce compares the Nonce against a potential hash of it
|
||||
func (s *SessionState) CheckNonce(hashed string) bool {
|
||||
return encryption.CheckNonce(s.Nonce, hashed)
|
||||
|
|
|
|||
|
|
@ -222,6 +222,24 @@ func TestEncodeAndDecodeSessionState(t *testing.T) {
|
|||
Nonce: []byte("abcdef1234567890abcdef1234567890"),
|
||||
Groups: []string{"group-a", "group-b"},
|
||||
},
|
||||
"With additional claims": {
|
||||
Email: "username@example.com",
|
||||
User: "username",
|
||||
PreferredUsername: "preferred.username",
|
||||
AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
CreatedAt: &created,
|
||||
ExpiresOn: &expires,
|
||||
RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
Nonce: []byte("abcdef1234567890abcdef1234567890"),
|
||||
Groups: []string{"group-a", "group-b"},
|
||||
AdditionalClaims: map[string]interface{}{
|
||||
"custom_claim_1": "value1",
|
||||
"custom_claim_2": true,
|
||||
"custom_claim_3": int8(1),
|
||||
"custom_claim_4": []interface{}{"item1", "item2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, secretSize := range []int{16, 24, 32} {
|
||||
|
|
@ -289,3 +307,52 @@ func compareSessionStates(t *testing.T, expected *SessionState, actual *SessionS
|
|||
act.ExpiresOn = nil
|
||||
assert.Equal(t, exp, act)
|
||||
}
|
||||
|
||||
func TestGetClaim(t *testing.T) {
|
||||
createdAt := time.Now()
|
||||
expiresOn := createdAt.Add(1 * time.Hour)
|
||||
|
||||
ss := &SessionState{
|
||||
CreatedAt: &createdAt,
|
||||
ExpiresOn: &expiresOn,
|
||||
AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7",
|
||||
Email: "user@example.com",
|
||||
User: "user123",
|
||||
Groups: []string{"group1", "group2"},
|
||||
PreferredUsername: "preferred_user",
|
||||
AdditionalClaims: map[string]interface{}{
|
||||
"custom_claim_1": "value1",
|
||||
"custom_claim_2": true,
|
||||
"custom_claim_3": 1,
|
||||
"custom_claim_4": []string{"item1", "item2"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
claim string
|
||||
want []string
|
||||
}{
|
||||
{"access_token", []string{"AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7"}},
|
||||
{"id_token", []string{"IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7"}},
|
||||
{"refresh_token", []string{"RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7"}},
|
||||
{"created_at", []string{createdAt.String()}},
|
||||
{"expires_on", []string{expiresOn.String()}},
|
||||
{"email", []string{"user@example.com"}},
|
||||
{"user", []string{"user123"}},
|
||||
{"groups", []string{"group1", "group2"}},
|
||||
{"preferred_username", []string{"preferred_user"}},
|
||||
{"custom_claim_1", []string{"value1"}},
|
||||
{"custom_claim_2", []string{"true"}},
|
||||
{"custom_claim_3", []string{"1"}},
|
||||
{"custom_claim_4", []string{"item1", "item2"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.claim, func(t *testing.T) {
|
||||
gs := NewWithT(t)
|
||||
gs.Expect(ss.GetClaim(tt.claim)).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ type ProviderData struct {
|
|||
EmailClaim string
|
||||
GroupsClaim string
|
||||
Verifier internaloidc.IDTokenVerifier
|
||||
AdditionalClaims []string `json:"additionalClaims,omitempty"`
|
||||
SkipClaimsFromProfileURL bool
|
||||
|
||||
// Universal Group authorization data structure
|
||||
|
|
@ -268,6 +269,11 @@ func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*
|
|||
}
|
||||
}
|
||||
|
||||
// Extract additional claims
|
||||
if p.AdditionalClaims != nil {
|
||||
p.extractAdditionalClaims(extractor, ss)
|
||||
}
|
||||
|
||||
// `email_verified` must be present and explicitly set to `false` to be
|
||||
// considered unverified.
|
||||
verifyEmail := (p.EmailClaim == options.OIDCEmailClaim) && !p.AllowUnverifiedEmail
|
||||
|
|
@ -301,6 +307,17 @@ func (p *ProviderData) getClaimExtractor(rawIDToken, accessToken string) (util.C
|
|||
return extractor, nil
|
||||
}
|
||||
|
||||
func (p *ProviderData) extractAdditionalClaims(extractor util.ClaimExtractor, ss *sessions.SessionState) {
|
||||
if ss.AdditionalClaims == nil {
|
||||
ss.AdditionalClaims = make(map[string]interface{})
|
||||
}
|
||||
for _, claim := range p.AdditionalClaims {
|
||||
if value, exists, err := extractor.GetClaim(claim); err == nil && exists {
|
||||
ss.AdditionalClaims[claim] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkNonce compares the session's nonce with the IDToken's nonce claim
|
||||
func (p *ProviderData) checkNonce(s *sessions.SessionState) error {
|
||||
extractor, err := p.getClaimExtractor(s.IDToken, "")
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
ExpectedError error
|
||||
ExpectedSession *sessions.SessionState
|
||||
ExpectProfileURLCalled bool
|
||||
AdditionalClaims []string
|
||||
}{
|
||||
"Standard": {
|
||||
IDToken: defaultIDToken,
|
||||
|
|
@ -417,6 +418,27 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
SkipClaimsFromProfileURL: true,
|
||||
ExpectedSession: &sessions.SessionState{},
|
||||
},
|
||||
"Additional claims": {
|
||||
IDToken: defaultIDToken,
|
||||
AdditionalClaims: []string{"phone_number", "picture"},
|
||||
ExpectedSession: &sessions.SessionState{
|
||||
PreferredUsername: "Jane Dobbs",
|
||||
AdditionalClaims: map[string]interface{}{
|
||||
"phone_number": "+4798765432",
|
||||
"picture": "http://mugbook.com/janed/me.jpg",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Additional claims with missing claim": {
|
||||
IDToken: defaultIDToken,
|
||||
AdditionalClaims: []string{"phone_number", "picture1"},
|
||||
ExpectedSession: &sessions.SessionState{
|
||||
PreferredUsername: "Jane Dobbs",
|
||||
AdditionalClaims: map[string]interface{}{
|
||||
"phone_number": "+4798765432",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for testName, tc := range testCases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
|
|
@ -453,6 +475,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
|
|||
provider.EmailClaim = tc.EmailClaim
|
||||
provider.GroupsClaim = tc.GroupsClaim
|
||||
provider.SkipClaimsFromProfileURL = tc.SkipClaimsFromProfileURL
|
||||
provider.AdditionalClaims = tc.AdditionalClaims
|
||||
|
||||
rawIDToken, err := newSignedTestIDToken(tc.IDToken)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ func newProviderDataFromConfig(providerConfig options.Provider) (*ProviderData,
|
|||
ClientSecret: providerConfig.ClientSecret,
|
||||
ClientSecretFile: providerConfig.ClientSecretFile,
|
||||
AuthRequestResponseMode: providerConfig.AuthRequestResponseMode,
|
||||
// additional claims to be extracted from the ID Token
|
||||
AdditionalClaims: providerConfig.AdditionalClaims,
|
||||
}
|
||||
|
||||
needsVerifier, err := providerRequiresOIDCProviderVerifier(providerConfig.Type)
|
||||
|
|
|
|||
Loading…
Reference in New Issue