refactor: use struct types for claim related types (#283)
* oidc: add regression tests for token claim json this helps to verify that the same JSON is produced, after these types are refactored. * refactor: use struct types for claim related types BREAKING CHANGE: The following types are changed from interface to struct type: - AccessTokenClaims - IDTokenClaims - IntrospectionResponse - UserInfo and related types. The following methods of OPStorage now take a pointer to a struct type, instead of an interface: - SetUserinfoFromScopes - SetUserinfoFromToken - SetIntrospectionFromToken The following functions are now generic, so that type-safe extension of Claims is now possible: - op.VerifyIDTokenHint - op.VerifyAccessToken - rp.VerifyTokens - rp.VerifyIDToken - Changed UserInfoAddress to pointer in UserInfo and IntrospectionResponse. This was needed to make omitempty work correctly. - Copy or merge maps in IntrospectionResponse and SetUserInfo * op: add example for VerifyAccessToken * fix: rp: wrong assignment in WithIssuedAtMaxAge WithIssuedAtMaxAge assigned its value to v.maxAge, which was wrong. This change fixes that by assiging the duration to v.maxAgeIAT. * rp: add VerifyTokens example * oidc: add standard references to: - IDTokenClaims - IntrospectionResponse - UserInfo * only count coverage for `./pkg/...`
This commit is contained in:
parent
4bd2b742f9
commit
dea8bc96ea
55 changed files with 2358 additions and 1516 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: ['1.16', '1.17', '1.18', '1.19', '1.20']
|
go: ['1.18', '1.19', '1.20']
|
||||||
name: Go ${{ matrix.go }} test
|
name: Go ${{ matrix.go }} test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
@ -24,7 +24,7 @@ jobs:
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- run: go test -race -v -coverprofile=profile.cov -coverpkg=github.com/zitadel/oidc/... ./pkg/...
|
- run: go test -race -v -coverprofile=profile.cov -coverpkg=./pkg/... ./pkg/...
|
||||||
- uses: codecov/codecov-action@v3.1.1
|
- uses: codecov/codecov-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
file: ./profile.cov
|
file: ./profile.cov
|
||||||
|
|
|
@ -98,9 +98,7 @@ Versions that also build are marked with :warning:.
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------|--------------------|
|
|---------|--------------------|
|
||||||
| <1.16 | :x: |
|
| <1.18 | :x: |
|
||||||
| 1.16 | :warning: |
|
|
||||||
| 1.17 | :warning: |
|
|
||||||
| 1.18 | :warning: |
|
| 1.18 | :warning: |
|
||||||
| 1.19 | :white_check_mark: |
|
| 1.19 | :white_check_mark: |
|
||||||
| 1.20 | :white_check_mark: |
|
| 1.20 | :white_check_mark: |
|
||||||
|
|
|
@ -76,7 +76,7 @@ func main() {
|
||||||
params := mux.Vars(r)
|
params := mux.Vars(r)
|
||||||
requestedClaim := params["claim"]
|
requestedClaim := params["claim"]
|
||||||
requestedValue := params["value"]
|
requestedValue := params["value"]
|
||||||
value, ok := resp.GetClaim(requestedClaim).(string)
|
value, ok := resp.Claims[requestedClaim].(string)
|
||||||
if !ok || value == "" || value != requestedValue {
|
if !ok || value == "" || value != requestedValue {
|
||||||
http.Error(w, "claim does not match", http.StatusForbidden)
|
http.Error(w, "claim does not match", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
|
|
|
@ -60,7 +60,7 @@ func main() {
|
||||||
http.Handle("/login", rp.AuthURLHandler(state, provider))
|
http.Handle("/login", rp.AuthURLHandler(state, provider))
|
||||||
|
|
||||||
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
// for demonstration purposes the returned userinfo response is written as JSON object onto response
|
||||||
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
data, err := json.Marshal(info)
|
data, err := json.Marshal(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
"github.com/zitadel/oidc/v2/pkg/client/rp/cli"
|
"github.com/zitadel/oidc/v2/pkg/client/rp/cli"
|
||||||
"github.com/zitadel/oidc/v2/pkg/http"
|
"github.com/zitadel/oidc/v2/pkg/http"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -43,7 +44,7 @@ func main() {
|
||||||
state := func() string {
|
state := func() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
}
|
}
|
||||||
token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, state)
|
token := cli.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, callbackPath, port, state)
|
||||||
|
|
||||||
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
|
client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token))
|
||||||
|
|
||||||
|
|
|
@ -429,13 +429,13 @@ func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientS
|
||||||
|
|
||||||
// SetUserinfoFromScopes implements the op.Storage interface
|
// SetUserinfoFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
||||||
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
|
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
|
||||||
return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
|
return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromToken implements the op.Storage interface
|
// SetUserinfoFromToken implements the op.Storage interface
|
||||||
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
|
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
|
||||||
token, ok := func() (*Token, bool) {
|
token, ok := func() (*Token, bool) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
@ -463,7 +463,7 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserIn
|
||||||
|
|
||||||
// SetIntrospectionFromToken implements the op.Storage interface
|
// SetIntrospectionFromToken implements the op.Storage interface
|
||||||
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
token, ok := func() (*Token, bool) {
|
token, ok := func() (*Token, bool) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
|
@ -480,14 +480,17 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o
|
||||||
// this will automatically be done by the library if you don't return an error
|
// this will automatically be done by the library if you don't return an error
|
||||||
// you can also return further information about the user / associated token
|
// you can also return further information about the user / associated token
|
||||||
// e.g. the userinfo (equivalent to userinfo endpoint)
|
// e.g. the userinfo (equivalent to userinfo endpoint)
|
||||||
err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes)
|
|
||||||
|
userInfo := new(oidc.UserInfo)
|
||||||
|
err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
introspection.SetUserInfo(userInfo)
|
||||||
//...and also the requested scopes...
|
//...and also the requested scopes...
|
||||||
introspection.SetScopes(token.Scopes)
|
introspection.Scope = token.Scopes
|
||||||
//...and the client the token was issued to
|
//...and the client the token was issued to
|
||||||
introspection.SetClientID(token.ApplicationID)
|
introspection.ClientID = token.ApplicationID
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -608,7 +611,7 @@ func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, aud
|
||||||
}
|
}
|
||||||
|
|
||||||
// setUserinfo sets the info based on the user, scopes and if necessary the clientID
|
// setUserinfo sets the info based on the user, scopes and if necessary the clientID
|
||||||
func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) {
|
func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, clientID string, scopes []string) (err error) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
defer s.lock.Unlock()
|
defer s.lock.Unlock()
|
||||||
user := s.userStore.GetUserByID(userID)
|
user := s.userStore.GetUserByID(userID)
|
||||||
|
@ -618,17 +621,19 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter,
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case oidc.ScopeOpenID:
|
case oidc.ScopeOpenID:
|
||||||
userInfo.SetSubject(user.ID)
|
userInfo.Subject = user.ID
|
||||||
case oidc.ScopeEmail:
|
case oidc.ScopeEmail:
|
||||||
userInfo.SetEmail(user.Email, user.EmailVerified)
|
userInfo.Email = user.Email
|
||||||
|
userInfo.EmailVerified = oidc.Bool(user.EmailVerified)
|
||||||
case oidc.ScopeProfile:
|
case oidc.ScopeProfile:
|
||||||
userInfo.SetPreferredUsername(user.Username)
|
userInfo.PreferredUsername = user.Username
|
||||||
userInfo.SetName(user.FirstName + " " + user.LastName)
|
userInfo.Name = user.FirstName + " " + user.LastName
|
||||||
userInfo.SetFamilyName(user.LastName)
|
userInfo.FamilyName = user.LastName
|
||||||
userInfo.SetGivenName(user.FirstName)
|
userInfo.GivenName = user.FirstName
|
||||||
userInfo.SetLocale(user.PreferredLanguage)
|
userInfo.Locale = oidc.NewLocale(user.PreferredLanguage)
|
||||||
case oidc.ScopePhone:
|
case oidc.ScopePhone:
|
||||||
userInfo.SetPhone(user.Phone, user.PhoneVerified)
|
userInfo.PhoneNumber = user.Phone
|
||||||
|
userInfo.PhoneNumberVerified = user.PhoneVerified
|
||||||
case CustomScope:
|
case CustomScope:
|
||||||
// you can also have a custom scope and assert public or custom claims based on that
|
// you can also have a custom scope and assert public or custom claims based on that
|
||||||
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
|
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
|
||||||
|
@ -698,7 +703,7 @@ func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context,
|
||||||
// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
|
||||||
// it will be called for the creation of an id_token - we are using the same private function as for other flows,
|
// it will be called for the creation of an id_token - we are using the same private function as for other flows,
|
||||||
// plus adding token exchange specific claims related to delegation or impersonation
|
// plus adding token exchange specific claims related to delegation or impersonation
|
||||||
func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request op.TokenExchangeRequest) error {
|
func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error {
|
||||||
err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
|
err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -198,7 +198,7 @@ func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, cl
|
||||||
|
|
||||||
// SetUserinfoFromScopes implements the op.Storage interface
|
// SetUserinfoFromScopes implements the op.Storage interface
|
||||||
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
||||||
func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
|
func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error {
|
||||||
storage, err := s.storageFromContext(ctx)
|
storage, err := s.storageFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -208,7 +208,7 @@ func (s *multiStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.
|
||||||
|
|
||||||
// SetUserinfoFromToken implements the op.Storage interface
|
// SetUserinfoFromToken implements the op.Storage interface
|
||||||
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
|
func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
|
||||||
storage, err := s.storageFromContext(ctx)
|
storage, err := s.storageFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -218,7 +218,7 @@ func (s *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.U
|
||||||
|
|
||||||
// SetIntrospectionFromToken implements the op.Storage interface
|
// SetIntrospectionFromToken implements the op.Storage interface
|
||||||
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
||||||
func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
func (s *multiStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||||
storage, err := s.storageFromContext(ctx)
|
storage, err := s.storageFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
22
go.mod
22
go.mod
|
@ -1,22 +1,36 @@
|
||||||
module github.com/zitadel/oidc/v2
|
module github.com/zitadel/oidc/v2
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/go-cmp v0.5.2 // indirect
|
|
||||||
github.com/google/go-github/v31 v31.0.0
|
github.com/google/go-github/v31 v31.0.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/gorilla/schema v1.2.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/jeremija/gosubmit v0.2.7
|
github.com/jeremija/gosubmit v0.2.7
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/muhlemmer/gu v0.3.0
|
||||||
github.com/rs/cors v1.8.3
|
github.com/rs/cors v1.8.3
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
|
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
|
||||||
golang.org/x/text v0.6.0
|
golang.org/x/text v0.6.0
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.4.2 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.0.0 // indirect
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||||
|
google.golang.org/appengine v1.6.6 // indirect
|
||||||
|
google.golang.org/protobuf v1.25.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
11
go.sum
11
go.sum
|
@ -123,6 +123,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/muhlemmer/gu v0.3.0 h1:UwNv9xXGp1WDgHKgk7ljjh3duh1w4ZAY1k1NsWBYl3Y=
|
||||||
|
github.com/muhlemmer/gu v0.3.0/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
@ -146,7 +148,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
@ -190,7 +191,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -217,7 +217,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
@ -237,7 +236,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -266,19 +264,15 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
@ -325,7 +319,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
|
||||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
58
internal/testutil/gen/gen.go
Normal file
58
internal/testutil/gen/gen.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Package gen allows generating of example tokens and claims.
|
||||||
|
//
|
||||||
|
// go run ./internal/testutil/gen
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var custom = map[string]any{
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
"bar": struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}{
|
||||||
|
Count: 22,
|
||||||
|
Tags: []string{"some", "tags"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
accessToken, atClaims := tu.NewAccessTokenCustom(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidJWTID,
|
||||||
|
tu.ValidClientID, tu.ValidSkew, custom,
|
||||||
|
)
|
||||||
|
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, idClaims := tu.NewIDTokenCustom(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.AddDate(99, 0, 0), tu.ValidAuthTime,
|
||||||
|
tu.ValidNonce, tu.ValidACR, tu.ValidAMR, tu.ValidClientID,
|
||||||
|
tu.ValidSkew, atHash, custom,
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println("access token claims:")
|
||||||
|
if err := enc.Encode(atClaims); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("access token:\n%s\n", accessToken)
|
||||||
|
|
||||||
|
fmt.Println("ID token claims:")
|
||||||
|
if err := enc.Encode(idClaims); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("ID token:\n%s\n", idToken)
|
||||||
|
}
|
146
internal/testutil/token.go
Normal file
146
internal/testutil/token.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Package testuril helps setting up required data for testing,
|
||||||
|
// such as tokens, claims and verifiers.
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeySet implements oidc.Keys
|
||||||
|
type KeySet struct{}
|
||||||
|
|
||||||
|
// VerifySignature implments op.KeySet.
|
||||||
|
func (KeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jws.Verify(WebKey.Public())
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a reproducible signing key
|
||||||
|
const webkeyJSON = `{"kty":"RSA","kid":"1","alg":"PS512","n":"x6JoG8t2Li68JSwPwnh51TvHYFf3z72tQ3wmJG3VosU6MdJF0gSTCIwflOJ38OWE6hYtN1WAeyBy2CYdnXd1QZzkK_apGK4M7hsNA9jCTg8NOZjLPL0ww1jp7313Skla7mbm90uNdg4TUNp2n_r-sCYywI-9cfSlhzLSksxKK_BRdzy6xW20daAcI-mErQXIcvdYIguunJk_uTb8kJedsWMcQ4Mb57QujUok2Z2YabWyb9Fi1_StixXJvd_WEu93SHNMORB0u6ymnO3aZJdATLdhtcP-qsVicQhffpqVazmZQPf7K-7n4I5vJE4g9XXzZ2dSKSp3Ewe_nna_2kvbCw","e":"AQAB","d":"sl3F_QeF2O-CxQegMRYpbL6Tfd47GM6VDxXOkn_cACmNvFPudB4ILPvdf830cjTv06Lq1WS8fcZZNgygK0A_cNc3-pvRK67e-KMMtuIlgU7rdwmwlN1Iw1Ee-w6z1ZjC-PzR4iQMCW28DmKS2I-OnV4TvH7xOe7nMmvTPrvujV__YKfUxvAWXJG7_wtaJBGplezn5nNsKG2Ot9h0mhMdYUgGC36wLxo3Q5d4m79EXQYdhm89EfxogwvMmHRes5PNpHRuDZRHGAI4RZi2KvgmqF07e1Qdq4TqbQnY5pCYrdjqvEFFjGC6jTE-ak_b21FcSVy-9aZHyf04U4g5-cIUEQ","p":"7AaicFryJCHRekdSkx8tfPxaSiyEuN8jhP9cLqs4rLkIbrSHmanPhjnLe-Tlh3icQ8hPoy6WC8ktLwsrzbfGIh4U_zgAfvtD1Y_lZM-YSWZsxqlrGiI5do11iVzzoy4a1XdkgOjHQz9y6J-uoA9jY8ILG7VaEZQnaYwWZV3cspk","q":"2Ide9hlwthXJQJYqI0mibM5BiGBxJ4CafPmF1DYNXggBCczZ6ERGReNTGM_AEhy5mvLXUH6uBSOJlfHTYzx49C1GgIO3hEWVEGAKAytVRL6RfAkVSOXMQUp-HjXKpGg_Nx1SJxQf3rulbW8HXO4KqIlloyIXpPQSK7jB8A4hJUM","dp":"1nmc6F4sRNsaQHRJO_mL21RxM4_KtzfFThjCCoJ6iLHHUNnpkp_1PTKNjrLMRFM8JHgErfMqU-FmlqYfEtvZRq1xRQ39nWX0GT-eIwJljuVtGQVglqnc77bRxJXbqz-9EJdik6VzVM92Op7IDxiMp1zvvSkJhInNWqL6wvgNEZk","dq":"dlHizlAwiw90ndpwxD-khhhfLwqkSpW31br0KnYu78cn6hcKrCVC0UXbTp-XsU4JDmbMyauvpBc7Q7iVbpDI94UWFXvkeF8diYkxb3HqclpAXasI-oC4EKWILTHvvc9JW_Clx7zzfV7Ekvws5dcd8-LAq1gh232TwFiBgY_3BMk","qi":"E1k_9W3odXgcmIP2PCJztE7hB7jeuAL1ElAY88VJBBPY670uwOEjKL2VfQuz9q9IjzLAvcgf7vS9blw2RHP_XqHqSOlJWGwvMQTF0Q8zLknCgKt8q7HQQNWIJcBZ8qdUVn02-qf4E3tgZ3JHaHNs8imA_L-__WoUmzC4z5jH_lM"}`
|
||||||
|
|
||||||
|
const SignatureAlgorithm = jose.RS256
|
||||||
|
|
||||||
|
var (
|
||||||
|
WebKey jose.JSONWebKey
|
||||||
|
Signer jose.Signer
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := json.Unmarshal([]byte(webkeyJSON), &WebKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
Signer, err = jose.NewSigner(jose.SigningKey{Algorithm: SignatureAlgorithm, Key: WebKey}, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signEncodeTokenClaims(claims any) string {
|
||||||
|
payload, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
object, err := Signer.Sign(payload)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
token, err := object.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimsMap(claims any) map[string]any {
|
||||||
|
data, err := json.Marshal(claims)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dst := make(map[string]any)
|
||||||
|
if err = json.Unmarshal(data, &dst); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDTokenCustom(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string, custom map[string]any) (string, *oidc.IDTokenClaims) {
|
||||||
|
claims := oidc.NewIDTokenClaims(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew)
|
||||||
|
claims.AccessTokenHash = atHash
|
||||||
|
claims.Claims = custom
|
||||||
|
token := signEncodeTokenClaims(claims)
|
||||||
|
|
||||||
|
// set this so that assertion in tests will work
|
||||||
|
claims.SignatureAlg = SignatureAlgorithm
|
||||||
|
claims.Claims = claimsMap(claims)
|
||||||
|
return token, claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIDToken creates a new IDTokenClaims with passed data and returns a signed token and claims.
|
||||||
|
func NewIDToken(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration, atHash string) (string, *oidc.IDTokenClaims) {
|
||||||
|
return NewIDTokenCustom(issuer, subject, audience, expiration, authTime, nonce, acr, amr, clientID, skew, atHash, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessTokenCustom(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration, custom map[string]any) (string, *oidc.AccessTokenClaims) {
|
||||||
|
claims := oidc.NewAccessTokenClaims(issuer, subject, audience, expiration, jwtid, clientID, skew)
|
||||||
|
claims.Claims = custom
|
||||||
|
token := signEncodeTokenClaims(claims)
|
||||||
|
|
||||||
|
// set this so that assertion in tests will work
|
||||||
|
claims.SignatureAlg = SignatureAlgorithm
|
||||||
|
claims.Claims = claimsMap(claims)
|
||||||
|
return token, claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAcccessToken creates a new AccessTokenClaims with passed data and returns a signed token and claims.
|
||||||
|
func NewAccessToken(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) (string, *oidc.AccessTokenClaims) {
|
||||||
|
return NewAccessTokenCustom(issuer, subject, audience, expiration, jwtid, clientID, skew, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InvalidSignatureToken = `eyJhbGciOiJQUzUxMiJ9.eyJpc3MiOiJsb2NhbC5jb20iLCJzdWIiOiJ0aW1AbG9jYWwuY29tIiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImV4cCI6MTY3Nzg0MDQzMSwiaWF0IjoxNjc3ODQwMzcwLCJhdXRoX3RpbWUiOjE2Nzc4NDAzMTAsIm5vbmNlIjoiMTIzNDUiLCJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF6cCI6IjU1NTY2NiJ9.DtZmvVkuE4Hw48ijBMhRJbxEWCr_WEYuPQBMY73J9TP6MmfeNFkjVJf4nh4omjB9gVLnQ-xhEkNOe62FS5P0BB2VOxPuHZUj34dNspCgG3h98fGxyiMb5vlIYAHDF9T-w_LntlYItohv63MmdYR-hPpAqjXE7KOfErf-wUDGE9R3bfiQ4HpTdyFJB1nsToYrZ9lhP2mzjTCTs58ckZfQ28DFHn_lfHWpR4rJBgvLx7IH4rMrUayr09Ap-PxQLbv0lYMtmgG1z3JK8MXnuYR0UJdZnEIezOzUTlThhCXB-nvuAXYjYxZZTR0FtlgZUHhIpYK0V2abf_Q_Or36akNCUg`
|
||||||
|
|
||||||
|
// These variables always result in a valid token
|
||||||
|
var (
|
||||||
|
ValidIssuer = "local.com"
|
||||||
|
ValidSubject = "tim@local.com"
|
||||||
|
ValidAudience = []string{"unit", "test"}
|
||||||
|
ValidAuthTime = time.Now().Add(-time.Minute) // authtime is always 1 minute in the past
|
||||||
|
ValidExpiration = ValidAuthTime.Add(2 * time.Minute) // token is always 1 more minute available
|
||||||
|
ValidJWTID = "9876"
|
||||||
|
ValidNonce = "12345"
|
||||||
|
ValidACR = "something"
|
||||||
|
ValidAMR = []string{"foo", "bar"}
|
||||||
|
ValidClientID = "555666"
|
||||||
|
ValidSkew = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidIDToken returns a token and claims that are in the token.
|
||||||
|
// It uses the Valid* global variables and the token will always
|
||||||
|
// pass verification.
|
||||||
|
func ValidIDToken() (string, *oidc.IDTokenClaims) {
|
||||||
|
return NewIDToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidAuthTime, ValidNonce, ValidACR, ValidAMR, ValidClientID, ValidSkew, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidAccessToken returns a token and claims that are in the token.
|
||||||
|
// It uses the Valid* global variables and the token always passes
|
||||||
|
// verification within the same test run.
|
||||||
|
func ValidAccessToken() (string, *oidc.AccessTokenClaims) {
|
||||||
|
return NewAccessToken(ValidIssuer, ValidSubject, ValidAudience, ValidExpiration, ValidJWTID, ValidClientID, ValidSkew)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACRVerify is a oidc.ACRVerifier func.
|
||||||
|
func ACRVerify(acr string) error {
|
||||||
|
if acr != ValidACR {
|
||||||
|
return errors.New("invalid acr")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -176,8 +176,8 @@ func SignedJWTProfileAssertion(clientID string, audience []string, expiration ti
|
||||||
Issuer: clientID,
|
Issuer: clientID,
|
||||||
Subject: clientID,
|
Subject: clientID,
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
ExpiresAt: oidc.Time(exp),
|
ExpiresAt: oidc.FromTime(exp),
|
||||||
IssuedAt: oidc.Time(iat),
|
IssuedAt: oidc.FromTime(iat),
|
||||||
}, signer)
|
}, signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -235,19 +235,19 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
}
|
}
|
||||||
|
|
||||||
var email string
|
var email string
|
||||||
redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
|
redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
|
||||||
require.NotNil(t, tokens, "tokens")
|
require.NotNil(t, tokens, "tokens")
|
||||||
require.NotNil(t, info, "info")
|
require.NotNil(t, info, "info")
|
||||||
t.Log("access token", tokens.AccessToken)
|
t.Log("access token", tokens.AccessToken)
|
||||||
t.Log("refresh token", tokens.RefreshToken)
|
t.Log("refresh token", tokens.RefreshToken)
|
||||||
t.Log("id token", tokens.IDToken)
|
t.Log("id token", tokens.IDToken)
|
||||||
t.Log("email", info.GetEmail())
|
t.Log("email", info.Email)
|
||||||
|
|
||||||
accessToken = tokens.AccessToken
|
accessToken = tokens.AccessToken
|
||||||
refreshToken = tokens.RefreshToken
|
refreshToken = tokens.RefreshToken
|
||||||
idToken = tokens.IDToken
|
idToken = tokens.IDToken
|
||||||
email = info.GetEmail()
|
email = info.Email
|
||||||
http.Redirect(w, r, targetURL, http.StatusFound)
|
http.Redirect(w, r, targetURL, 302)
|
||||||
}
|
}
|
||||||
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
|
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider)(capturedW, get)
|
||||||
|
|
||||||
|
@ -258,7 +258,6 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID,
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
||||||
require.Less(t, capturedW.Code, 400, "token exchange response code")
|
|
||||||
|
|
||||||
//nolint:bodyclose
|
//nolint:bodyclose
|
||||||
resp = capturedW.Result()
|
resp = capturedW.Result()
|
||||||
|
|
|
@ -13,13 +13,13 @@ const (
|
||||||
loginPath = "/login"
|
loginPath = "/login"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CodeFlow(ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
|
func CodeFlow[C oidc.IDClaims](ctx context.Context, relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens[C] {
|
||||||
codeflowCtx, codeflowCancel := context.WithCancel(ctx)
|
codeflowCtx, codeflowCancel := context.WithCancel(ctx)
|
||||||
defer codeflowCancel()
|
defer codeflowCancel()
|
||||||
|
|
||||||
tokenChan := make(chan *oidc.Tokens, 1)
|
tokenChan := make(chan *oidc.Tokens[C], 1)
|
||||||
|
|
||||||
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
|
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp rp.RelyingParty) {
|
||||||
tokenChan <- tokens
|
tokenChan <- tokens
|
||||||
msg := "<p><strong>Success!</strong></p>"
|
msg := "<p><strong>Success!</strong></p>"
|
||||||
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
|
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
package mock
|
|
||||||
|
|
||||||
//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier
|
|
|
@ -1,163 +0,0 @@
|
||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: github.com/zitadel/oidc/v2/pkg/client/rp (interfaces: IDTokenVerifier)
|
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
|
||||||
time "time"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
oidc "github.com/zitadel/oidc/v2/pkg/oidc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockIDTokenVerifier is a mock of IDTokenVerifier interface.
|
|
||||||
type MockIDTokenVerifier struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockIDTokenVerifierMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockIDTokenVerifierMockRecorder is the mock recorder for MockIDTokenVerifier.
|
|
||||||
type MockIDTokenVerifierMockRecorder struct {
|
|
||||||
mock *MockIDTokenVerifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockIDTokenVerifier creates a new mock instance.
|
|
||||||
func NewMockIDTokenVerifier(ctrl *gomock.Controller) *MockIDTokenVerifier {
|
|
||||||
mock := &MockIDTokenVerifier{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockIDTokenVerifierMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockIDTokenVerifier) EXPECT() *MockIDTokenVerifierMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACR mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) ACR() oidc.ACRVerifier {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ACR")
|
|
||||||
ret0, _ := ret[0].(oidc.ACRVerifier)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACR indicates an expected call of ACR.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) ACR() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACR", reflect.TypeOf((*MockIDTokenVerifier)(nil).ACR))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientID mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) ClientID() string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ClientID")
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientID indicates an expected call of ClientID.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) ClientID() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockIDTokenVerifier)(nil).ClientID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) Issuer() string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Issuer")
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer indicates an expected call of Issuer.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) Issuer() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockIDTokenVerifier)(nil).Issuer))
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeySet mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) KeySet() oidc.KeySet {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "KeySet")
|
|
||||||
ret0, _ := ret[0].(oidc.KeySet)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeySet indicates an expected call of KeySet.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) KeySet() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockIDTokenVerifier)(nil).KeySet))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxAge mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) MaxAge() time.Duration {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "MaxAge")
|
|
||||||
ret0, _ := ret[0].(time.Duration)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxAge indicates an expected call of MaxAge.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) MaxAge() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAge", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAge))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxAgeIAT mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) MaxAgeIAT() time.Duration {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "MaxAgeIAT")
|
|
||||||
ret0, _ := ret[0].(time.Duration)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxAgeIAT indicates an expected call of MaxAgeIAT.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) MaxAgeIAT() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxAgeIAT", reflect.TypeOf((*MockIDTokenVerifier)(nil).MaxAgeIAT))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonce mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) Nonce(arg0 context.Context) string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Nonce", arg0)
|
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonce indicates an expected call of Nonce.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) Nonce(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nonce", reflect.TypeOf((*MockIDTokenVerifier)(nil).Nonce), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) Offset() time.Duration {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Offset")
|
|
||||||
ret0, _ := ret[0].(time.Duration)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset indicates an expected call of Offset.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) Offset() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Offset", reflect.TypeOf((*MockIDTokenVerifier)(nil).Offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportedSignAlgs mocks base method.
|
|
||||||
func (m *MockIDTokenVerifier) SupportedSignAlgs() []string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "SupportedSignAlgs")
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportedSignAlgs indicates an expected call of SupportedSignAlgs.
|
|
||||||
func (mr *MockIDTokenVerifierMockRecorder) SupportedSignAlgs() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedSignAlgs", reflect.TypeOf((*MockIDTokenVerifier)(nil).SupportedSignAlgs))
|
|
||||||
}
|
|
|
@ -373,7 +373,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (stri
|
||||||
|
|
||||||
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
// CodeExchange handles the oauth2 code exchange, extracting and validating the id_token
|
||||||
// returning it parsed together with the oauth2 tokens (access, refresh)
|
// returning it parsed together with the oauth2 tokens (access, refresh)
|
||||||
func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) {
|
func CodeExchange[C oidc.IDClaims](ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens[C], err error) {
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient())
|
||||||
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
codeOpts := make([]oauth2.AuthCodeOption, 0)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -386,7 +386,7 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod
|
||||||
}
|
}
|
||||||
|
|
||||||
if rp.IsOAuth2Only() {
|
if rp.IsOAuth2Only() {
|
||||||
return &oidc.Tokens{Token: token}, nil
|
return &oidc.Tokens[C]{Token: token}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
idTokenString, ok := token.Extra(idTokenKey).(string)
|
idTokenString, ok := token.Extra(idTokenKey).(string)
|
||||||
|
@ -394,20 +394,20 @@ func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...Cod
|
||||||
return nil, errors.New("id_token missing")
|
return nil, errors.New("id_token missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken, err := VerifyTokens(ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
idToken, err := VerifyTokens[C](ctx, token.AccessToken, idTokenString, rp.IDTokenVerifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
return &oidc.Tokens[C]{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty)
|
type CodeExchangeCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty)
|
||||||
|
|
||||||
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
// CodeExchangeHandler extends the `CodeExchange` method with a http handler
|
||||||
// including cookie handling for secure `state` transfer
|
// including cookie handling for secure `state` transfer
|
||||||
// and optional PKCE code verifier checking
|
// and optional PKCE code verifier checking
|
||||||
func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.HandlerFunc {
|
func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
state, err := tryReadStateCookie(w, r, rp)
|
state, err := tryReadStateCookie(w, r, rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -436,7 +436,7 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||||
}
|
}
|
||||||
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
|
tokens, err := CodeExchange[C](r.Context(), params.Get("code"), rp, codeOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -445,13 +445,13 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty) http.Ha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeExchangeUserinfoCallback func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, provider RelyingParty, info oidc.UserInfo)
|
type CodeExchangeUserinfoCallback[C oidc.IDClaims] func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, provider RelyingParty, info *oidc.UserInfo)
|
||||||
|
|
||||||
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
// UserinfoCallback wraps the callback function of the CodeExchangeHandler
|
||||||
// and calls the userinfo endpoint with the access token
|
// and calls the userinfo endpoint with the access token
|
||||||
// on success it will pass the userinfo into its callback function as well
|
// on success it will pass the userinfo into its callback function as well
|
||||||
func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback {
|
func UserinfoCallback[C oidc.IDClaims](f CodeExchangeUserinfoCallback[C]) CodeExchangeCallback[C] {
|
||||||
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp RelyingParty) {
|
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[C], state string, rp RelyingParty) {
|
||||||
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
info, err := Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), rp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "userinfo failed: "+err.Error(), http.StatusUnauthorized)
|
||||||
|
@ -462,17 +462,17 @@ func UserinfoCallback(f CodeExchangeUserinfoCallback) CodeExchangeCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
// Userinfo will call the OIDC Userinfo Endpoint with the provided token
|
||||||
func Userinfo(token, tokenType, subject string, rp RelyingParty) (oidc.UserInfo, error) {
|
func Userinfo(token, tokenType, subject string, rp RelyingParty) (*oidc.UserInfo, error) {
|
||||||
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("authorization", tokenType+" "+token)
|
req.Header.Set("authorization", tokenType+" "+token)
|
||||||
userinfo := oidc.NewUserInfo()
|
userinfo := new(oidc.UserInfo)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if userinfo.GetSubject() != subject {
|
if userinfo.Subject != subject {
|
||||||
return nil, ErrUserInfoSubNotMatching
|
return nil, ErrUserInfoSubNotMatching
|
||||||
}
|
}
|
||||||
return userinfo, nil
|
return userinfo, nil
|
||||||
|
|
|
@ -21,69 +21,71 @@ type IDTokenVerifier interface {
|
||||||
|
|
||||||
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
// VerifyTokens implement the Token Response Validation as defined in OIDC specification
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
|
||||||
func VerifyTokens(ctx context.Context, accessToken, idTokenString string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
|
func VerifyTokens[C oidc.IDClaims](ctx context.Context, accessToken, idToken string, v IDTokenVerifier) (claims C, err error) {
|
||||||
idToken, err := VerifyIDToken(ctx, idTokenString, v)
|
var nilClaims C
|
||||||
|
|
||||||
|
claims, err = VerifyIDToken[C](ctx, idToken, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
if err := VerifyAccessToken(accessToken, idToken.GetAccessTokenHash(), idToken.GetSignatureAlgorithm()); err != nil {
|
if err := VerifyAccessToken(accessToken, claims.GetAccessTokenHash(), claims.GetSignatureAlgorithm()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
return idToken, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyIDToken validates the id token according to
|
// VerifyIDToken validates the id token according to
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
func VerifyIDToken(ctx context.Context, token string, v IDTokenVerifier) (oidc.IDTokenClaims, error) {
|
func VerifyIDToken[C oidc.Claims](ctx context.Context, token string, v IDTokenVerifier) (claims C, err error) {
|
||||||
claims := oidc.EmptyIDTokenClaims()
|
var nilClaims C
|
||||||
|
|
||||||
decrypted, err := oidc.DecryptToken(token)
|
decrypted, err := oidc.DecryptToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
payload, err := oidc.ParseToken(decrypted, claims)
|
payload, err := oidc.ParseToken(decrypted, &claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := oidc.CheckSubject(claims); err != nil {
|
if err := oidc.CheckSubject(claims); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
if err = oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
if err = oidc.CheckAudience(claims, v.ClientID()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
if err = oidc.CheckAuthorizedParty(claims, v.ClientID()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
if err = oidc.CheckNonce(claims, v.Nonce(ctx)); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
@ -112,7 +114,7 @@ func NewIDTokenVerifier(issuer, clientID string, keySet oidc.KeySet, options ...
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
keySet: keySet,
|
keySet: keySet,
|
||||||
offset: 1 * time.Second,
|
offset: time.Second,
|
||||||
nonce: func(_ context.Context) string {
|
nonce: func(_ context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
|
@ -139,7 +141,7 @@ func WithIssuedAtOffset(offset time.Duration) func(*idTokenVerifier) {
|
||||||
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
// WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
||||||
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
func WithIssuedAtMaxAge(maxAge time.Duration) func(*idTokenVerifier) {
|
||||||
return func(v *idTokenVerifier) {
|
return func(v *idTokenVerifier) {
|
||||||
v.maxAge = maxAge
|
v.maxAgeIAT = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
339
pkg/client/rp/verifier_test.go
Normal file
339
pkg/client/rp/verifier_test.go
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
package rp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyTokens(t *testing.T) {
|
||||||
|
verifier := &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
maxAge: 2 * time.Minute,
|
||||||
|
acr: tu.ACRVerify,
|
||||||
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
}
|
||||||
|
accessToken, _ := tu.ValidAccessToken()
|
||||||
|
atHash, err := oidc.ClaimHash(accessToken, tu.SignatureAlgorithm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
accessToken string
|
||||||
|
idTokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "without access token",
|
||||||
|
idTokenClaims: tu.ValidIDToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with access token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired id token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, atHash,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong access token",
|
||||||
|
accessToken: accessToken,
|
||||||
|
idTokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "~~~",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
idToken, want := tt.idTokenClaims()
|
||||||
|
got, err := VerifyTokens[*oidc.IDTokenClaims](context.Background(), tt.accessToken, idToken, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyIDToken(t *testing.T) {
|
||||||
|
verifier := &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
maxAge: 2 * time.Minute,
|
||||||
|
acr: tu.ACRVerify,
|
||||||
|
nonce: func(context.Context) string { return tu.ValidNonce },
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clientID string
|
||||||
|
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: tu.ValidIDToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse err",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signature",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty subject",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, "", tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong issuer",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong clientID",
|
||||||
|
clientID: "foo",
|
||||||
|
tokenClaims: tu.ValidIDToken,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong IAT",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong acr",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired auth",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong nonce",
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, "foo",
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
token, want := tt.tokenClaims()
|
||||||
|
verifier.clientID = tt.clientID
|
||||||
|
got, err := VerifyIDToken[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyAccessToken(t *testing.T) {
|
||||||
|
token, _ := tu.ValidAccessToken()
|
||||||
|
hash, err := oidc.ClaimHash(token, tu.SignatureAlgorithm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
accessToken string
|
||||||
|
atHash string
|
||||||
|
sigAlgorithm jose.SignatureAlgorithm
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty hash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: hash,
|
||||||
|
sigAlgorithm: tu.SignatureAlgorithm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid algorithm",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: hash,
|
||||||
|
sigAlgorithm: "foo",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatch",
|
||||||
|
args: args{
|
||||||
|
accessToken: token,
|
||||||
|
atHash: "~~",
|
||||||
|
sigAlgorithm: tu.SignatureAlgorithm,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := VerifyAccessToken(tt.args.accessToken, tt.args.atHash, tt.args.sigAlgorithm)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIDTokenVerifier(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
issuer string
|
||||||
|
clientID string
|
||||||
|
keySet oidc.KeySet
|
||||||
|
options []VerifierOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want IDTokenVerifier
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil nonce", // otherwise assert.Equal will fail on the function
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
options: []VerifierOption{
|
||||||
|
WithIssuedAtOffset(time.Minute),
|
||||||
|
WithIssuedAtMaxAge(time.Hour),
|
||||||
|
WithNonce(nil), // otherwise assert.Equal will fail on the function
|
||||||
|
WithACRVerifier(nil),
|
||||||
|
WithAuthTimeMaxAge(2 * time.Hour),
|
||||||
|
WithSupportedSigningAlgorithms("ABC", "DEF"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &idTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
offset: time.Minute,
|
||||||
|
maxAgeIAT: time.Hour,
|
||||||
|
clientID: tu.ValidClientID,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
nonce: nil,
|
||||||
|
acr: nil,
|
||||||
|
maxAge: 2 * time.Hour,
|
||||||
|
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := NewIDTokenVerifier(tt.args.issuer, tt.args.clientID, tt.args.keySet, tt.args.options...)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
86
pkg/client/rp/verifier_tokens_example_test.go
Normal file
86
pkg/client/rp/verifier_tokens_example_test.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package rp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyCustomClaims extends the TokenClaims base,
|
||||||
|
// so it implmeents the oidc.Claims interface.
|
||||||
|
// Instead of carrying a map, we add needed fields// to the struct for type safe access.
|
||||||
|
type MyCustomClaims struct {
|
||||||
|
oidc.TokenClaims
|
||||||
|
NotBefore oidc.Time `json:"nbf,omitempty"`
|
||||||
|
AccessTokenHash string `json:"at_hash,omitempty"`
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
Bar *Nested `json:"bar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessTokenHash is required to implement
|
||||||
|
// the oidc.IDClaims interface.
|
||||||
|
func (c *MyCustomClaims) GetAccessTokenHash() string {
|
||||||
|
return c.AccessTokenHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested struct types are also possible.
|
||||||
|
type Nested struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
idToken carries the following claims. foo and bar are custom claims
|
||||||
|
|
||||||
|
{
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"at_hash": "2dzbm_vIxy-7eRtqUIGPPw",
|
||||||
|
"aud": [
|
||||||
|
"unit",
|
||||||
|
"test",
|
||||||
|
"555666"
|
||||||
|
],
|
||||||
|
"auth_time": 1678100961,
|
||||||
|
"azp": "555666",
|
||||||
|
"bar": {
|
||||||
|
"count": 22,
|
||||||
|
"tags": [
|
||||||
|
"some",
|
||||||
|
"tags"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_id": "555666",
|
||||||
|
"exp": 4802238682,
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
"iat": 1678101021,
|
||||||
|
"iss": "local.com",
|
||||||
|
"jti": "9876",
|
||||||
|
"nbf": 1678101021,
|
||||||
|
"nonce": "12345",
|
||||||
|
"sub": "tim@local.com"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const idToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhY3IiOiJzb21ldGhpbmciLCJhbXIiOlsiZm9vIiwiYmFyIl0sImF0X2hhc2giOiIyZHpibV92SXh5LTdlUnRxVUlHUFB3IiwiYXVkIjpbInVuaXQiLCJ0ZXN0IiwiNTU1NjY2Il0sImF1dGhfdGltZSI6MTY3ODEwMDk2MSwiYXpwIjoiNTU1NjY2IiwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiY2xpZW50X2lkIjoiNTU1NjY2IiwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJub25jZSI6IjEyMzQ1Iiwic3ViIjoidGltQGxvY2FsLmNvbSJ9.t3GXSfVNNwiW1Suv9_84v0sdn2_-RWHVxhphhRozDXnsO7SDNOlGnEioemXABESxSzMclM7gB7mYy5Qah2ZUNx7eP5t2njoxEYfavgHwx7UJZ2NCg8NDPQyr-hlxelEcfdXK-I0oTd-FRDvF4rqPkD9Us52IpnplChCxnHFgh4wKwPqZZjv2IXVCtn0ilKW3hff1rMOYKEuLRcN2YP0gkyuqyHvcf2dMmjod0t4sLOTJ82rsCbMBC5CLpqv3nIC9HOGITkt1Kd-Am0n1LrdZvWwTo6RFe8AnzF0gpqjcB5Wg4Qeh58DIjZOz4f_8wnmJ_gCqyRh5vfSW4XHdbum0Tw`
|
||||||
|
const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM4NjgyLCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MTAxMDIxLCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MTAxMDIxLCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.Zrz3LWSRjCMJZUMaI5dUbW4vGdSmEeJQ3ouhaX0bcW9rdFFLgBI4K2FWJhNivq8JDmCGSxwLu3mI680GWmDaEoAx1M5sCO9lqfIZHGZh-lfAXk27e6FPLlkTDBq8Bx4o4DJ9Fw0hRJGjUTjnYv5cq1vo2-UqldasL6CwTbkzNC_4oQFfRtuodC4Ql7dZ1HRv5LXuYx7KPkOssLZtV9cwtJp5nFzKjcf2zEE_tlbjcpynMwypornRUp1EhCWKRUGkJhJeiP71ECY5pQhShfjBu9Nc5wDpSnZmnk2S4YsPrRK3QkE-iEkas8BfsOCrGoErHjEJexAIDjasGO5PFLWfCA`
|
||||||
|
|
||||||
|
func ExampleVerifyTokens_customClaims() {
|
||||||
|
v := rp.NewIDTokenVerifier("local.com", "555666", tu.KeySet{},
|
||||||
|
rp.WithNonce(func(ctx context.Context) string { return "12345" }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyAccessToken can be called with the *MyCustomClaims.
|
||||||
|
claims, err := rp.VerifyTokens[*MyCustomClaims](context.TODO(), accessToken, idToken, v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Here we have typesafe access to the custom claims
|
||||||
|
fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags)
|
||||||
|
// Output: Hello, World! 22 [some tags]
|
||||||
|
}
|
|
@ -112,7 +112,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
|
func Introspect(ctx context.Context, rp ResourceServer, token string) (*oidc.IntrospectionResponse, error) {
|
||||||
authFn, err := rp.AuthFn()
|
authFn, err := rp.AuthFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -121,7 +121,7 @@ func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.Intr
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp := oidc.NewIntrospectionResponse()
|
resp := new(oidc.IntrospectionResponse)
|
||||||
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
if err := httphelper.HttpRequest(rp.HttpClient(), req, resp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import "github.com/muhlemmer/gu"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IntrospectionRequest struct {
|
type IntrospectionRequest struct {
|
||||||
Token string `schema:"token"`
|
Token string `schema:"token"`
|
||||||
|
@ -17,36 +11,11 @@ type ClientAssertionParams struct {
|
||||||
ClientAssertionType string `schema:"client_assertion_type"`
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IntrospectionResponse interface {
|
// IntrospectionResponse implements RFC 7662, section 2.2 and
|
||||||
UserInfoSetter
|
// OpenID Connect Core 1.0, section 5.1 (UserInfo).
|
||||||
IsActive() bool
|
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2.
|
||||||
SetActive(bool)
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||||
SetScopes(scopes []string)
|
type IntrospectionResponse struct {
|
||||||
SetClientID(id string)
|
|
||||||
SetTokenType(tokenType string)
|
|
||||||
SetExpiration(exp time.Time)
|
|
||||||
SetIssuedAt(iat time.Time)
|
|
||||||
SetNotBefore(nbf time.Time)
|
|
||||||
SetAudience(audience []string)
|
|
||||||
SetIssuer(issuer string)
|
|
||||||
SetJWTID(id string)
|
|
||||||
GetScope() []string
|
|
||||||
GetClientID() string
|
|
||||||
GetTokenType() string
|
|
||||||
GetExpiration() time.Time
|
|
||||||
GetIssuedAt() time.Time
|
|
||||||
GetNotBefore() time.Time
|
|
||||||
GetSubject() string
|
|
||||||
GetAudience() []string
|
|
||||||
GetIssuer() string
|
|
||||||
GetJWTID() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIntrospectionResponse() IntrospectionResponse {
|
|
||||||
return &introspectionResponse{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type introspectionResponse struct {
|
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
Scope SpaceDelimitedArray `json:"scope,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
@ -58,323 +27,50 @@ type introspectionResponse struct {
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
JWTID string `json:"jti,omitempty"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
userInfoProfile
|
Username string `json:"username,omitempty"`
|
||||||
userInfoEmail
|
UserInfoProfile
|
||||||
userInfoPhone
|
UserInfoEmail
|
||||||
|
UserInfoPhone
|
||||||
|
|
||||||
Address UserInfoAddress `json:"address,omitempty"`
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
claims map[string]interface{}
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) IsActive() bool {
|
// SetUserInfo copies all relevant fields from UserInfo
|
||||||
return i.Active
|
// into the IntroSpectionResponse.
|
||||||
|
func (i *IntrospectionResponse) SetUserInfo(u *UserInfo) {
|
||||||
|
i.Subject = u.Subject
|
||||||
|
i.Username = u.PreferredUsername
|
||||||
|
i.Address = gu.PtrCopy(u.Address)
|
||||||
|
i.UserInfoProfile = u.UserInfoProfile
|
||||||
|
i.UserInfoEmail = u.UserInfoEmail
|
||||||
|
i.UserInfoPhone = u.UserInfoPhone
|
||||||
|
if i.Claims == nil {
|
||||||
|
i.Claims = gu.MapCopy(u.Claims)
|
||||||
|
} else {
|
||||||
|
gu.MapMerge(u.Claims, i.Claims)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) GetSubject() string {
|
// GetAddress is a safe getter that takes
|
||||||
return i.Subject
|
// care of a possible nil value.
|
||||||
}
|
func (i *IntrospectionResponse) GetAddress() *UserInfoAddress {
|
||||||
|
if i.Address == nil {
|
||||||
func (i *introspectionResponse) GetName() string {
|
return new(UserInfoAddress)
|
||||||
return i.Name
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetGivenName() string {
|
|
||||||
return i.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetFamilyName() string {
|
|
||||||
return i.FamilyName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetMiddleName() string {
|
|
||||||
return i.MiddleName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetNickname() string {
|
|
||||||
return i.Nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetProfile() string {
|
|
||||||
return i.Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPicture() string {
|
|
||||||
return i.Picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetWebsite() string {
|
|
||||||
return i.Website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetGender() Gender {
|
|
||||||
return i.Gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetBirthdate() string {
|
|
||||||
return i.Birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetZoneinfo() string {
|
|
||||||
return i.Zoneinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetLocale() language.Tag {
|
|
||||||
return i.Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPreferredUsername() string {
|
|
||||||
return i.PreferredUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetEmail() string {
|
|
||||||
return i.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) IsEmailVerified() bool {
|
|
||||||
return bool(i.EmailVerified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetPhoneNumber() string {
|
|
||||||
return i.PhoneNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) IsPhoneNumberVerified() bool {
|
|
||||||
return i.PhoneNumberVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetAddress() UserInfoAddress {
|
|
||||||
return i.Address
|
return i.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClaim(key string) interface{} {
|
// introspectionResponseAlias prevents loops on the JSON methods
|
||||||
return i.claims[key]
|
type introspectionResponseAlias IntrospectionResponse
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClaims() map[string]interface{} {
|
func (i *IntrospectionResponse) MarshalJSON() ([]byte, error) {
|
||||||
return i.claims
|
if i.Username == "" {
|
||||||
}
|
i.Username = i.PreferredUsername
|
||||||
|
|
||||||
func (i *introspectionResponse) GetScope() []string {
|
|
||||||
return []string(i.Scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetClientID() string {
|
|
||||||
return i.ClientID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetTokenType() string {
|
|
||||||
return i.TokenType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetExpiration() time.Time {
|
|
||||||
return time.Time(i.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(i.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetNotBefore() time.Time {
|
|
||||||
return time.Time(i.NotBefore)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetAudience() []string {
|
|
||||||
return []string(i.Audience)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetIssuer() string {
|
|
||||||
return i.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) GetJWTID() string {
|
|
||||||
return i.JWTID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetActive(active bool) {
|
|
||||||
i.Active = active
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetScopes(scope []string) {
|
|
||||||
i.Scope = scope
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetClientID(id string) {
|
|
||||||
i.ClientID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetTokenType(tokenType string) {
|
|
||||||
i.TokenType = tokenType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetExpiration(exp time.Time) {
|
|
||||||
i.Expiration = Time(exp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetIssuedAt(iat time.Time) {
|
|
||||||
i.IssuedAt = Time(iat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetNotBefore(nbf time.Time) {
|
|
||||||
i.NotBefore = Time(nbf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetAudience(audience []string) {
|
|
||||||
i.Audience = audience
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetIssuer(issuer string) {
|
|
||||||
i.Issuer = issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetJWTID(id string) {
|
|
||||||
i.JWTID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetSubject(sub string) {
|
|
||||||
i.Subject = sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetName(name string) {
|
|
||||||
i.Name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetGivenName(name string) {
|
|
||||||
i.GivenName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetFamilyName(name string) {
|
|
||||||
i.FamilyName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetMiddleName(name string) {
|
|
||||||
i.MiddleName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetNickname(name string) {
|
|
||||||
i.Nickname = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetUpdatedAt(date time.Time) {
|
|
||||||
i.UpdatedAt = Time(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetProfile(profile string) {
|
|
||||||
i.Profile = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPicture(picture string) {
|
|
||||||
i.Picture = picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetWebsite(website string) {
|
|
||||||
i.Website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetGender(gender Gender) {
|
|
||||||
i.Gender = gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetBirthdate(birthdate string) {
|
|
||||||
i.Birthdate = birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetZoneinfo(zoneInfo string) {
|
|
||||||
i.Zoneinfo = zoneInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetLocale(locale language.Tag) {
|
|
||||||
i.Locale = locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPreferredUsername(name string) {
|
|
||||||
i.PreferredUsername = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetEmail(email string, verified bool) {
|
|
||||||
i.Email = email
|
|
||||||
i.EmailVerified = boolString(verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetPhone(phone string, verified bool) {
|
|
||||||
i.PhoneNumber = phone
|
|
||||||
i.PhoneNumberVerified = verified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) SetAddress(address UserInfoAddress) {
|
|
||||||
i.Address = address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) AppendClaims(key string, value interface{}) {
|
|
||||||
if i.claims == nil {
|
|
||||||
i.claims = make(map[string]interface{})
|
|
||||||
}
|
}
|
||||||
i.claims[key] = value
|
return mergeAndMarshalClaims((*introspectionResponseAlias)(i), i.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *introspectionResponse) MarshalJSON() ([]byte, error) {
|
func (i *IntrospectionResponse) UnmarshalJSON(data []byte) error {
|
||||||
type Alias introspectionResponse
|
return unmarshalJSONMulti(data, (*introspectionResponseAlias)(i), &i.Claims)
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
Locale interface{} `json:"locale,omitempty"`
|
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(i),
|
|
||||||
}
|
|
||||||
if !i.Locale.IsRoot() {
|
|
||||||
a.Locale = i.Locale
|
|
||||||
}
|
|
||||||
if !time.Time(i.UpdatedAt).IsZero() {
|
|
||||||
a.UpdatedAt = time.Time(i.UpdatedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.Expiration).IsZero() {
|
|
||||||
a.Expiration = time.Time(i.Expiration).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.IssuedAt).IsZero() {
|
|
||||||
a.IssuedAt = time.Time(i.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(i.NotBefore).IsZero() {
|
|
||||||
a.NotBefore = time.Time(i.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
a.Username = i.PreferredUsername
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(i.claims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &i.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(i.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *introspectionResponse) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias introspectionResponse
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
UpdatedAt int64 `json:"update_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(i),
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &i.claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
78
pkg/oidc/introspection_test.go
Normal file
78
pkg/oidc/introspection_test.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_SetUserInfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
start *IntrospectionResponse
|
||||||
|
want *IntrospectionResponse
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
|
||||||
|
name: "nil claims",
|
||||||
|
start: &IntrospectionResponse{},
|
||||||
|
want: &IntrospectionResponse{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
Username: userInfoData.PreferredUsername,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Claims: userInfoData.Claims,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
name: "merge claims",
|
||||||
|
start: &IntrospectionResponse{
|
||||||
|
Claims: map[string]any{
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &IntrospectionResponse{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
Username: userInfoData.PreferredUsername,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Claims: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tt.start.SetUserInfo(userInfoData)
|
||||||
|
assert.Equal(t, tt.want, tt.start)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_GetAddress(t *testing.T) {
|
||||||
|
// nil address
|
||||||
|
i := new(IntrospectionResponse)
|
||||||
|
assert.Equal(t, &UserInfoAddress{}, i.GetAddress())
|
||||||
|
|
||||||
|
i.Address = &UserInfoAddress{PostalCode: "1234"}
|
||||||
|
assert.Equal(t, i.Address, i.GetAddress())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntrospectionResponse_MarshalJSON(t *testing.T) {
|
||||||
|
got, err := json.Marshal(&IntrospectionResponse{
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
PreferredUsername: "muhlemmer",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, string(got), `{"active":false,"username":"muhlemmer","preferred_username":"muhlemmer"}`)
|
||||||
|
}
|
50
pkg/oidc/regression_assert_test.go
Normal file
50
pkg/oidc/regression_assert_test.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
//go:build !create_regression_data
|
||||||
|
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_assert_regression verifies current output from
|
||||||
|
// json.Marshal to stored regression data.
|
||||||
|
// These tests are only ran when the create_regression_data
|
||||||
|
// tag is NOT set.
|
||||||
|
func Test_assert_regression(t *testing.T) {
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
|
||||||
|
for _, obj := range regressionData {
|
||||||
|
name := jsonFilename(obj)
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
file, err := os.Open(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(buf, file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
want := buf.String()
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
encodeJSON(t, buf, obj)
|
||||||
|
first := buf.String()
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
assert.JSONEq(t, want, first)
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
json.Unmarshal([]byte(first), obj),
|
||||||
|
)
|
||||||
|
second, err := json.Marshal(obj)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.JSONEq(t, want, string(second))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
24
pkg/oidc/regression_create_test.go
Normal file
24
pkg/oidc/regression_create_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//go:build create_regression_data
|
||||||
|
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_create_regression generates the regression data.
|
||||||
|
// It is excluded from regular testing, unless
|
||||||
|
// called with the create_regression_data tag:
|
||||||
|
// go test -tags="create_regression_data" ./pkg/oidc
|
||||||
|
func Test_create_regression(t *testing.T) {
|
||||||
|
for _, obj := range regressionData {
|
||||||
|
file, err := os.Create(jsonFilename(obj))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encodeJSON(t, file, obj)
|
||||||
|
}
|
||||||
|
}
|
26
pkg/oidc/regression_data/oidc.AccessTokenClaims.json
Normal file
26
pkg/oidc/regression_data/oidc.AccessTokenClaims.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"iss": "zitadel",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"jti": "900",
|
||||||
|
"azp": "just@me.com",
|
||||||
|
"nonce": "6969",
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"some",
|
||||||
|
"methods"
|
||||||
|
],
|
||||||
|
"scope": [
|
||||||
|
"email",
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"client_id": "777",
|
||||||
|
"exp": 12345,
|
||||||
|
"iat": 12000,
|
||||||
|
"nbf": 12000,
|
||||||
|
"auth_time": 12000,
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
51
pkg/oidc/regression_data/oidc.IDTokenClaims.json
Normal file
51
pkg/oidc/regression_data/oidc.IDTokenClaims.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"iss": "zitadel",
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"jti": "900",
|
||||||
|
"azp": "just@me.com",
|
||||||
|
"nonce": "6969",
|
||||||
|
"at_hash": "acthashhash",
|
||||||
|
"c_hash": "hashhash",
|
||||||
|
"acr": "something",
|
||||||
|
"amr": [
|
||||||
|
"some",
|
||||||
|
"methods"
|
||||||
|
],
|
||||||
|
"sid": "666",
|
||||||
|
"client_id": "777",
|
||||||
|
"exp": 12345,
|
||||||
|
"iat": 12000,
|
||||||
|
"nbf": 12000,
|
||||||
|
"auth_time": 12000,
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"updated_at": 1,
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
44
pkg/oidc/regression_data/oidc.IntrospectionResponse.json
Normal file
44
pkg/oidc/regression_data/oidc.IntrospectionResponse.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"client_id": "777",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 12345,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"iat": 12000,
|
||||||
|
"iss": "zitadel",
|
||||||
|
"jti": "900",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nbf": 12000,
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"scope": "email phone",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"token_type": "idtoken",
|
||||||
|
"updated_at": 1,
|
||||||
|
"username": "muhlemmer",
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
11
pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
Normal file
11
pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"aud": [
|
||||||
|
"foo",
|
||||||
|
"bar"
|
||||||
|
],
|
||||||
|
"exp": 12345,
|
||||||
|
"foo": "bar",
|
||||||
|
"iat": 12000,
|
||||||
|
"iss": "zitadel",
|
||||||
|
"sub": "hello@me.com"
|
||||||
|
}
|
30
pkg/oidc/regression_data/oidc.UserInfo.json
Normal file
30
pkg/oidc/regression_data/oidc.UserInfo.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"address": {
|
||||||
|
"country": "Moon",
|
||||||
|
"formatted": "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
"locality": "Smallvile",
|
||||||
|
"postal_code": "666-666",
|
||||||
|
"region": "Outer space",
|
||||||
|
"street_address": "Sesame street 666"
|
||||||
|
},
|
||||||
|
"birthdate": "1st of April",
|
||||||
|
"email": "tim@zitadel.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"family_name": "Möhlmann",
|
||||||
|
"foo": "bar",
|
||||||
|
"gender": "male",
|
||||||
|
"given_name": "Tim",
|
||||||
|
"locale": "nl",
|
||||||
|
"middle_name": "Danger",
|
||||||
|
"name": "Tim Möhlmann",
|
||||||
|
"nickname": "muhlemmer",
|
||||||
|
"phone_number": "+1234567890",
|
||||||
|
"phone_number_verified": true,
|
||||||
|
"picture": "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
"preferred_username": "muhlemmer",
|
||||||
|
"profile": "https://github.com/muhlemmer",
|
||||||
|
"sub": "hello@me.com",
|
||||||
|
"updated_at": 1,
|
||||||
|
"website": "https://zitadel.com",
|
||||||
|
"zoneinfo": "Europe/Amsterdam"
|
||||||
|
}
|
40
pkg/oidc/regression_test.go
Normal file
40
pkg/oidc/regression_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
// This file contains common functions and data for regression testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dataDir = "regression_data"
|
||||||
|
|
||||||
|
// jsonFilename builds a filename for the regression testdata.
|
||||||
|
// dataDir/<type_name>.json
|
||||||
|
func jsonFilename(obj interface{}) string {
|
||||||
|
name := fmt.Sprintf("%T.json", obj)
|
||||||
|
return path.Join(
|
||||||
|
dataDir,
|
||||||
|
strings.TrimPrefix(name, "*"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeJSON(t *testing.T, w io.Writer, obj interface{}) {
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
require.NoError(t, enc.Encode(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
var regressionData = []interface{}{
|
||||||
|
accessTokenData,
|
||||||
|
idTokenData,
|
||||||
|
introspectionResponseData,
|
||||||
|
userInfoData,
|
||||||
|
jwtProfileAssertionData,
|
||||||
|
}
|
|
@ -2,15 +2,13 @@ package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"os"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/zitadel/oidc/v2/pkg/crypto"
|
"github.com/zitadel/oidc/v2/pkg/crypto"
|
||||||
"github.com/zitadel/oidc/v2/pkg/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -20,380 +18,174 @@ const (
|
||||||
PrefixBearer = BearerToken + " "
|
PrefixBearer = BearerToken + " "
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tokens struct {
|
type Tokens[C IDClaims] struct {
|
||||||
*oauth2.Token
|
*oauth2.Token
|
||||||
IDTokenClaims IDTokenClaims
|
IDTokenClaims C
|
||||||
IDToken string
|
IDToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessTokenClaims interface {
|
// TokenClaims contains the base Claims used all tokens.
|
||||||
Claims
|
// It implements OpenID Connect Core 1.0, section 2.
|
||||||
GetSubject() string
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||||
GetTokenID() string
|
// And RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens,
|
||||||
SetPrivateClaims(map[string]interface{})
|
// section 2.2. https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure
|
||||||
GetClaims() map[string]interface{}
|
//
|
||||||
}
|
// TokenClaims implements the Claims interface,
|
||||||
|
// and can be used to extend larger claim types by embedding.
|
||||||
type IDTokenClaims interface {
|
type TokenClaims struct {
|
||||||
Claims
|
|
||||||
GetNotBefore() time.Time
|
|
||||||
GetJWTID() string
|
|
||||||
GetAccessTokenHash() string
|
|
||||||
GetCodeHash() string
|
|
||||||
GetAuthenticationMethodsReferences() []string
|
|
||||||
GetClientID() string
|
|
||||||
GetSignatureAlgorithm() jose.SignatureAlgorithm
|
|
||||||
SetAccessTokenHash(hash string)
|
|
||||||
SetUserinfo(userinfo UserInfo)
|
|
||||||
SetCodeHash(hash string)
|
|
||||||
UserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmptyAccessTokenClaims() AccessTokenClaims {
|
|
||||||
return new(accessTokenClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims {
|
|
||||||
now := time.Now().UTC().Add(-skew)
|
|
||||||
if len(audience) == 0 {
|
|
||||||
audience = append(audience, clientID)
|
|
||||||
}
|
|
||||||
return &accessTokenClaims{
|
|
||||||
Issuer: issuer,
|
|
||||||
Subject: subject,
|
|
||||||
Audience: audience,
|
|
||||||
Expiration: Time(expiration),
|
|
||||||
IssuedAt: Time(now),
|
|
||||||
NotBefore: Time(now),
|
|
||||||
JWTID: id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type accessTokenClaims struct {
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
Issuer string `json:"iss,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
Audience Audience `json:"aud,omitempty"`
|
Audience Audience `json:"aud,omitempty"`
|
||||||
Expiration Time `json:"exp,omitempty"`
|
Expiration Time `json:"exp,omitempty"`
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
IssuedAt Time `json:"iat,omitempty"`
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
AuthorizedParty string `json:"azp,omitempty"`
|
|
||||||
Nonce string `json:"nonce,omitempty"`
|
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
AuthTime Time `json:"auth_time,omitempty"`
|
||||||
CodeHash string `json:"c_hash,omitempty"`
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
|
Nonce string `json:"nonce,omitempty"`
|
||||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
||||||
SessionID string `json:"sid,omitempty"`
|
|
||||||
Scopes []string `json:"scope,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
|
|
||||||
|
|
||||||
claims map[string]interface{} `json:"-"`
|
|
||||||
signatureAlg jose.SignatureAlgorithm `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetIssuer() string {
|
|
||||||
return a.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudience implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetAudience() []string {
|
|
||||||
return a.Audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetExpiration() time.Time {
|
|
||||||
return time.Time(a.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(a.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetNonce() string {
|
|
||||||
return a.Nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthenticationContextClassReference implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetAuthenticationContextClassReference() string {
|
|
||||||
return a.AuthenticationContextClassReference
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthTime implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetAuthTime() time.Time {
|
|
||||||
return time.Time(a.AuthTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthorizedParty implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) GetAuthorizedParty() string {
|
|
||||||
return a.AuthorizedParty
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSignatureAlgorithm implements the Claims interface
|
|
||||||
func (a *accessTokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
|
||||||
a.signatureAlg = algorithm
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSubject implements the AccessTokenClaims interface
|
|
||||||
func (a *accessTokenClaims) GetSubject() string {
|
|
||||||
return a.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTokenID implements the AccessTokenClaims interface
|
|
||||||
func (a *accessTokenClaims) GetTokenID() string {
|
|
||||||
return a.JWTID
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPrivateClaims implements the AccessTokenClaims interface
|
|
||||||
func (a *accessTokenClaims) SetPrivateClaims(claims map[string]interface{}) {
|
|
||||||
a.claims = claims
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClaims implements the AccessTokenClaims interface
|
|
||||||
func (a *accessTokenClaims) GetClaims() map[string]interface{} {
|
|
||||||
return a.claims
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accessTokenClaims) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias accessTokenClaims
|
|
||||||
s := &struct {
|
|
||||||
*Alias
|
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
AuthTime int64 `json:"auth_time,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(a),
|
|
||||||
}
|
|
||||||
if !time.Time(a.Expiration).IsZero() {
|
|
||||||
s.Expiration = time.Time(a.Expiration).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(a.IssuedAt).IsZero() {
|
|
||||||
s.IssuedAt = time.Time(a.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(a.NotBefore).IsZero() {
|
|
||||||
s.NotBefore = time.Time(a.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(a.AuthTime).IsZero() {
|
|
||||||
s.AuthTime = time.Time(a.AuthTime).Unix()
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.claims == nil {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
info, err := json.Marshal(a.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return http.ConcatenateJSON(b, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accessTokenClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias accessTokenClaims
|
|
||||||
if err := json.Unmarshal(data, (*Alias)(a)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
claims := make(map[string]interface{})
|
|
||||||
if err := json.Unmarshal(data, &claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.claims = claims
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmptyIDTokenClaims() IDTokenClaims {
|
|
||||||
return new(idTokenClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) IDTokenClaims {
|
|
||||||
audience = AppendClientIDToAudience(clientID, audience)
|
|
||||||
return &idTokenClaims{
|
|
||||||
Issuer: issuer,
|
|
||||||
Audience: audience,
|
|
||||||
Expiration: Time(expiration),
|
|
||||||
IssuedAt: Time(time.Now().UTC().Add(-skew)),
|
|
||||||
AuthTime: Time(authTime.Add(-skew)),
|
|
||||||
Nonce: nonce,
|
|
||||||
AuthenticationContextClassReference: acr,
|
|
||||||
AuthenticationMethodsReferences: amr,
|
|
||||||
AuthorizedParty: clientID,
|
|
||||||
UserInfo: &userinfo{Subject: subject},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type idTokenClaims struct {
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
Audience Audience `json:"aud,omitempty"`
|
|
||||||
Expiration Time `json:"exp,omitempty"`
|
|
||||||
NotBefore Time `json:"nbf,omitempty"`
|
|
||||||
IssuedAt Time `json:"iat,omitempty"`
|
|
||||||
JWTID string `json:"jti,omitempty"`
|
|
||||||
AuthorizedParty string `json:"azp,omitempty"`
|
AuthorizedParty string `json:"azp,omitempty"`
|
||||||
Nonce string `json:"nonce,omitempty"`
|
|
||||||
AuthTime Time `json:"auth_time,omitempty"`
|
|
||||||
AccessTokenHash string `json:"at_hash,omitempty"`
|
|
||||||
CodeHash string `json:"c_hash,omitempty"`
|
|
||||||
AuthenticationContextClassReference string `json:"acr,omitempty"`
|
|
||||||
AuthenticationMethodsReferences []string `json:"amr,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
ClientID string `json:"client_id,omitempty"`
|
||||||
UserInfo `json:"-"`
|
JWTID string `json:"jti,omitempty"`
|
||||||
|
|
||||||
signatureAlg jose.SignatureAlgorithm
|
// Additional information set by this framework
|
||||||
|
SignatureAlg jose.SignatureAlgorithm `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface
|
func (c *TokenClaims) GetIssuer() string {
|
||||||
func (t *idTokenClaims) GetIssuer() string {
|
return c.Issuer
|
||||||
return t.Issuer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAudience implements the Claims interface
|
func (c *TokenClaims) GetSubject() string {
|
||||||
func (t *idTokenClaims) GetAudience() []string {
|
return c.Subject
|
||||||
return t.Audience
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
func (c *TokenClaims) GetAudience() []string {
|
||||||
func (t *idTokenClaims) GetExpiration() time.Time {
|
return c.Audience
|
||||||
return time.Time(t.Expiration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
func (c *TokenClaims) GetExpiration() time.Time {
|
||||||
func (t *idTokenClaims) GetIssuedAt() time.Time {
|
return c.Expiration.AsTime()
|
||||||
return time.Time(t.IssuedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
func (c *TokenClaims) GetIssuedAt() time.Time {
|
||||||
func (t *idTokenClaims) GetNonce() string {
|
return c.IssuedAt.AsTime()
|
||||||
return t.Nonce
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthenticationContextClassReference implements the Claims interface
|
func (c *TokenClaims) GetNonce() string {
|
||||||
func (t *idTokenClaims) GetAuthenticationContextClassReference() string {
|
return c.Nonce
|
||||||
return t.AuthenticationContextClassReference
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthTime implements the Claims interface
|
func (c *TokenClaims) GetAuthTime() time.Time {
|
||||||
func (t *idTokenClaims) GetAuthTime() time.Time {
|
return c.AuthTime.AsTime()
|
||||||
return time.Time(t.AuthTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthorizedParty implements the Claims interface
|
func (c *TokenClaims) GetAuthorizedParty() string {
|
||||||
func (t *idTokenClaims) GetAuthorizedParty() string {
|
return c.AuthorizedParty
|
||||||
return t.AuthorizedParty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSignatureAlgorithm implements the Claims interface
|
func (c *TokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
|
||||||
func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) {
|
return c.SignatureAlg
|
||||||
t.signatureAlg = alg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNotBefore implements the IDTokenClaims interface
|
func (c *TokenClaims) GetAuthenticationContextClassReference() string {
|
||||||
func (t *idTokenClaims) GetNotBefore() time.Time {
|
return c.AuthenticationContextClassReference
|
||||||
return time.Time(t.NotBefore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJWTID implements the IDTokenClaims interface
|
func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
||||||
func (t *idTokenClaims) GetJWTID() string {
|
c.SignatureAlg = algorithm
|
||||||
return t.JWTID
|
}
|
||||||
|
|
||||||
|
type AccessTokenClaims struct {
|
||||||
|
TokenClaims
|
||||||
|
Scopes []string `json:"scope,omitempty"`
|
||||||
|
Claims map[string]any `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, jwtid, clientID string, skew time.Duration) *AccessTokenClaims {
|
||||||
|
now := time.Now().UTC().Add(-skew)
|
||||||
|
if len(audience) == 0 {
|
||||||
|
audience = append(audience, clientID)
|
||||||
|
}
|
||||||
|
return &AccessTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: subject,
|
||||||
|
Audience: audience,
|
||||||
|
Expiration: FromTime(expiration),
|
||||||
|
IssuedAt: FromTime(now),
|
||||||
|
NotBefore: FromTime(now),
|
||||||
|
JWTID: jwtid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type atcAlias AccessTokenClaims
|
||||||
|
|
||||||
|
func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*atcAlias)(a), a.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
|
return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDTokenClaims extends TokenClaims by further implementing
|
||||||
|
// OpenID Connect Core 1.0, sections 3.1.3.6 (Code flow),
|
||||||
|
// 3.2.2.10 (implicit), 3.3.2.11 (Hybrid) and 5.1 (UserInfo).
|
||||||
|
// https://openid.net/specs/openid-connect-core-1_0.html#toc
|
||||||
|
type IDTokenClaims struct {
|
||||||
|
TokenClaims
|
||||||
|
NotBefore Time `json:"nbf,omitempty"`
|
||||||
|
AccessTokenHash string `json:"at_hash,omitempty"`
|
||||||
|
CodeHash string `json:"c_hash,omitempty"`
|
||||||
|
SessionID string `json:"sid,omitempty"`
|
||||||
|
UserInfoProfile
|
||||||
|
UserInfoEmail
|
||||||
|
UserInfoPhone
|
||||||
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessTokenHash implements the IDTokenClaims interface
|
// GetAccessTokenHash implements the IDTokenClaims interface
|
||||||
func (t *idTokenClaims) GetAccessTokenHash() string {
|
func (t *IDTokenClaims) GetAccessTokenHash() string {
|
||||||
return t.AccessTokenHash
|
return t.AccessTokenHash
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCodeHash implements the IDTokenClaims interface
|
func (t *IDTokenClaims) SetUserInfo(i *UserInfo) {
|
||||||
func (t *idTokenClaims) GetCodeHash() string {
|
t.Subject = i.Subject
|
||||||
return t.CodeHash
|
t.UserInfoProfile = i.UserInfoProfile
|
||||||
|
t.UserInfoEmail = i.UserInfoEmail
|
||||||
|
t.UserInfoPhone = i.UserInfoPhone
|
||||||
|
t.Address = i.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthenticationMethodsReferences implements the IDTokenClaims interface
|
func NewIDTokenClaims(issuer, subject string, audience []string, expiration, authTime time.Time, nonce string, acr string, amr []string, clientID string, skew time.Duration) *IDTokenClaims {
|
||||||
func (t *idTokenClaims) GetAuthenticationMethodsReferences() []string {
|
audience = AppendClientIDToAudience(clientID, audience)
|
||||||
return t.AuthenticationMethodsReferences
|
return &IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: subject,
|
||||||
|
Audience: audience,
|
||||||
|
Expiration: FromTime(expiration),
|
||||||
|
IssuedAt: FromTime(time.Now().Add(-skew)),
|
||||||
|
AuthTime: FromTime(authTime.Add(-skew)),
|
||||||
|
Nonce: nonce,
|
||||||
|
AuthenticationContextClassReference: acr,
|
||||||
|
AuthenticationMethodsReferences: amr,
|
||||||
|
AuthorizedParty: clientID,
|
||||||
|
ClientID: clientID,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientID implements the IDTokenClaims interface
|
type itcAlias IDTokenClaims
|
||||||
func (t *idTokenClaims) GetClientID() string {
|
|
||||||
return t.ClientID
|
func (i *IDTokenClaims) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*itcAlias)(i), i.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSignatureAlgorithm implements the IDTokenClaims interface
|
func (i *IDTokenClaims) UnmarshalJSON(data []byte) error {
|
||||||
func (t *idTokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
|
return unmarshalJSONMulti(data, (*itcAlias)(i), &i.Claims)
|
||||||
return t.signatureAlg
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAccessTokenHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetAccessTokenHash(hash string) {
|
|
||||||
t.AccessTokenHash = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUserinfo implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetUserinfo(info UserInfo) {
|
|
||||||
t.UserInfo = info
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCodeHash implements the IDTokenClaims interface
|
|
||||||
func (t *idTokenClaims) SetCodeHash(hash string) {
|
|
||||||
t.CodeHash = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *idTokenClaims) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias idTokenClaims
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Expiration int64 `json:"exp,omitempty"`
|
|
||||||
IssuedAt int64 `json:"iat,omitempty"`
|
|
||||||
NotBefore int64 `json:"nbf,omitempty"`
|
|
||||||
AuthTime int64 `json:"auth_time,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(t),
|
|
||||||
}
|
|
||||||
if !time.Time(t.Expiration).IsZero() {
|
|
||||||
a.Expiration = time.Time(t.Expiration).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.IssuedAt).IsZero() {
|
|
||||||
a.IssuedAt = time.Time(t.IssuedAt).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.NotBefore).IsZero() {
|
|
||||||
a.NotBefore = time.Time(t.NotBefore).Unix()
|
|
||||||
}
|
|
||||||
if !time.Time(t.AuthTime).IsZero() {
|
|
||||||
a.AuthTime = time.Time(t.AuthTime).Unix()
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.UserInfo == nil {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
info, err := json.Marshal(t.UserInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return http.ConcatenateJSON(b, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *idTokenClaims) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias idTokenClaims
|
|
||||||
if err := json.Unmarshal(data, (*Alias)(t)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
userinfo := new(userinfo)
|
|
||||||
if err := json.Unmarshal(data, userinfo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.UserInfo = userinfo
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessTokenResponse struct {
|
type AccessTokenResponse struct {
|
||||||
|
@ -405,19 +197,7 @@ type AccessTokenResponse struct {
|
||||||
State string `json:"state,omitempty" schema:"state,omitempty"`
|
State string `json:"state,omitempty" schema:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTProfileAssertionClaims interface {
|
type JWTProfileAssertionClaims struct {
|
||||||
GetKeyID() string
|
|
||||||
GetPrivateKey() []byte
|
|
||||||
GetIssuer() string
|
|
||||||
GetSubject() string
|
|
||||||
GetAudience() []string
|
|
||||||
GetExpiration() time.Time
|
|
||||||
GetIssuedAt() time.Time
|
|
||||||
SetCustomClaim(key string, value interface{})
|
|
||||||
GetCustomClaim(key string) interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type jwtProfileAssertion struct {
|
|
||||||
PrivateKeyID string `json:"-"`
|
PrivateKeyID string `json:"-"`
|
||||||
PrivateKey []byte `json:"-"`
|
PrivateKey []byte `json:"-"`
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"`
|
||||||
|
@ -426,91 +206,21 @@ type jwtProfileAssertion struct {
|
||||||
Expiration Time `json:"exp"`
|
Expiration Time `json:"exp"`
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
|
|
||||||
customClaims map[string]interface{}
|
Claims map[string]interface{} `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) {
|
type jpaAlias JWTProfileAssertionClaims
|
||||||
type Alias jwtProfileAssertion
|
|
||||||
a := (*Alias)(j)
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
func (j *JWTProfileAssertionClaims) MarshalJSON() ([]byte, error) {
|
||||||
if err != nil {
|
return mergeAndMarshalClaims((*jpaAlias)(j), j.Claims)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(j.customClaims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &j.customClaims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(j.customClaims)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error {
|
func (j *JWTProfileAssertionClaims) UnmarshalJSON(data []byte) error {
|
||||||
type Alias jwtProfileAssertion
|
return unmarshalJSONMulti(data, (*jpaAlias)(j), &j.Claims)
|
||||||
a := (*Alias)(j)
|
|
||||||
|
|
||||||
err := json.Unmarshal(data, a)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &j.customClaims)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetKeyID() string {
|
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
|
||||||
return j.PrivateKeyID
|
data, err := os.ReadFile(filename)
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetPrivateKey() []byte {
|
|
||||||
return j.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) {
|
|
||||||
if j.customClaims == nil {
|
|
||||||
j.customClaims = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
j.customClaims[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} {
|
|
||||||
if j.customClaims == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return j.customClaims[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetIssuer() string {
|
|
||||||
return j.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetSubject() string {
|
|
||||||
return j.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetAudience() []string {
|
|
||||||
return j.Audience
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetExpiration() time.Time {
|
|
||||||
return time.Time(j.Expiration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *jwtProfileAssertion) GetIssuedAt() time.Time {
|
|
||||||
return time.Time(j.IssuedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
|
||||||
data, err := ioutil.ReadFile(filename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -530,19 +240,19 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, op
|
||||||
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
|
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) {
|
func JWTProfileDelegatedSubject(sub string) func(*JWTProfileAssertionClaims) {
|
||||||
return func(j *jwtProfileAssertion) {
|
return func(j *JWTProfileAssertionClaims) {
|
||||||
j.Subject = sub
|
j.Subject = sub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) {
|
func JWTProfileCustomClaim(key string, value interface{}) func(*JWTProfileAssertionClaims) {
|
||||||
return func(j *jwtProfileAssertion) {
|
return func(j *JWTProfileAssertionClaims) {
|
||||||
j.customClaims[key] = value
|
j.Claims[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (*JWTProfileAssertionClaims, error) {
|
||||||
keyData := new(struct {
|
keyData := new(struct {
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -555,18 +265,18 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...
|
||||||
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
|
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssertionOption func(*jwtProfileAssertion)
|
type AssertionOption func(*JWTProfileAssertionClaims)
|
||||||
|
|
||||||
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims {
|
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) *JWTProfileAssertionClaims {
|
||||||
j := &jwtProfileAssertion{
|
j := &JWTProfileAssertionClaims{
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
PrivateKeyID: keyID,
|
PrivateKeyID: keyID,
|
||||||
Issuer: userID,
|
Issuer: userID,
|
||||||
Subject: userID,
|
Subject: userID,
|
||||||
IssuedAt: Time(time.Now().UTC()),
|
IssuedAt: FromTime(time.Now().UTC()),
|
||||||
Expiration: Time(time.Now().Add(1 * time.Hour).UTC()),
|
Expiration: FromTime(time.Now().Add(1 * time.Hour).UTC()),
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
customClaims: make(map[string]interface{}),
|
Claims: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -594,14 +304,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
return append(audience, clientID)
|
return append(audience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) {
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
|
||||||
privateKey, err := crypto.BytesToPrivateKey(assertion.GetPrivateKey())
|
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := jose.SigningKey{
|
key := jose.SigningKey{
|
||||||
Algorithm: jose.RS256,
|
Algorithm: jose.RS256,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
||||||
}
|
}
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -187,12 +187,12 @@ func (j *JWTTokenRequest) GetAudience() []string {
|
||||||
|
|
||||||
// GetExpiration implements the Claims interface
|
// GetExpiration implements the Claims interface
|
||||||
func (j *JWTTokenRequest) GetExpiration() time.Time {
|
func (j *JWTTokenRequest) GetExpiration() time.Time {
|
||||||
return time.Time(j.ExpiresAt)
|
return j.ExpiresAt.AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface
|
// GetIssuedAt implements the Claims interface
|
||||||
func (j *JWTTokenRequest) GetIssuedAt() time.Time {
|
func (j *JWTTokenRequest) GetIssuedAt() time.Time {
|
||||||
return time.Time(j.IssuedAt)
|
return j.ExpiresAt.AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNonce implements the Claims interface
|
// GetNonce implements the Claims interface
|
||||||
|
|
227
pkg/oidc/token_test.go
Normal file
227
pkg/oidc/token_test.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenClaimsData = TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
JWTID: "900",
|
||||||
|
AuthorizedParty: "just@me.com",
|
||||||
|
Nonce: "6969",
|
||||||
|
AuthTime: 12000,
|
||||||
|
NotBefore: 12000,
|
||||||
|
AuthenticationContextClassReference: "something",
|
||||||
|
AuthenticationMethodsReferences: []string{"some", "methods"},
|
||||||
|
ClientID: "777",
|
||||||
|
SignatureAlg: jose.ES256,
|
||||||
|
}
|
||||||
|
accessTokenData = &AccessTokenClaims{
|
||||||
|
TokenClaims: tokenClaimsData,
|
||||||
|
Scopes: []string{"email", "phone"},
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
idTokenData = &IDTokenClaims{
|
||||||
|
TokenClaims: tokenClaimsData,
|
||||||
|
NotBefore: 12000,
|
||||||
|
AccessTokenHash: "acthashhash",
|
||||||
|
CodeHash: "hashhash",
|
||||||
|
SessionID: "666",
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
introspectionResponseData = &IntrospectionResponse{
|
||||||
|
Active: true,
|
||||||
|
Scope: SpaceDelimitedArray{"email", "phone"},
|
||||||
|
ClientID: "777",
|
||||||
|
TokenType: "idtoken",
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
NotBefore: 12000,
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Issuer: "zitadel",
|
||||||
|
JWTID: "900",
|
||||||
|
Username: "muhlemmer",
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
userInfoData = &UserInfo{
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
Name: "Tim Möhlmann",
|
||||||
|
GivenName: "Tim",
|
||||||
|
FamilyName: "Möhlmann",
|
||||||
|
MiddleName: "Danger",
|
||||||
|
Nickname: "muhlemmer",
|
||||||
|
Profile: "https://github.com/muhlemmer",
|
||||||
|
Picture: "https://avatars.githubusercontent.com/u/5411563?v=4",
|
||||||
|
Website: "https://zitadel.com",
|
||||||
|
Gender: "male",
|
||||||
|
Birthdate: "1st of April",
|
||||||
|
Zoneinfo: "Europe/Amsterdam",
|
||||||
|
Locale: NewLocale(language.Dutch),
|
||||||
|
UpdatedAt: 1,
|
||||||
|
PreferredUsername: "muhlemmer",
|
||||||
|
},
|
||||||
|
UserInfoEmail: UserInfoEmail{
|
||||||
|
Email: "tim@zitadel.com",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoPhone: UserInfoPhone{
|
||||||
|
PhoneNumber: "+1234567890",
|
||||||
|
PhoneNumberVerified: true,
|
||||||
|
},
|
||||||
|
Address: &UserInfoAddress{
|
||||||
|
Formatted: "Sesame street 666\n666-666, Smallvile\nMoon",
|
||||||
|
StreetAddress: "Sesame street 666",
|
||||||
|
Locality: "Smallvile",
|
||||||
|
Region: "Outer space",
|
||||||
|
PostalCode: "666-666",
|
||||||
|
Country: "Moon",
|
||||||
|
},
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jwtProfileAssertionData = &JWTProfileAssertionClaims{
|
||||||
|
PrivateKeyID: "8888",
|
||||||
|
PrivateKey: []byte("qwerty"),
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "bar"},
|
||||||
|
Expiration: 12345,
|
||||||
|
IssuedAt: 12000,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTokenClaims(t *testing.T) {
|
||||||
|
claims := tokenClaimsData
|
||||||
|
|
||||||
|
assert.Equal(t, claims.Issuer, tokenClaimsData.GetIssuer())
|
||||||
|
assert.Equal(t, claims.Subject, tokenClaimsData.GetSubject())
|
||||||
|
assert.Equal(t, []string(claims.Audience), tokenClaimsData.GetAudience())
|
||||||
|
assert.Equal(t, claims.Expiration.AsTime(), tokenClaimsData.GetExpiration())
|
||||||
|
assert.Equal(t, claims.IssuedAt.AsTime(), tokenClaimsData.GetIssuedAt())
|
||||||
|
assert.Equal(t, claims.Nonce, tokenClaimsData.GetNonce())
|
||||||
|
assert.Equal(t, claims.AuthTime.AsTime(), tokenClaimsData.GetAuthTime())
|
||||||
|
assert.Equal(t, claims.AuthorizedParty, tokenClaimsData.GetAuthorizedParty())
|
||||||
|
assert.Equal(t, claims.SignatureAlg, tokenClaimsData.GetSignatureAlgorithm())
|
||||||
|
assert.Equal(t, claims.AuthenticationContextClassReference, tokenClaimsData.GetAuthenticationContextClassReference())
|
||||||
|
|
||||||
|
claims.SetSignatureAlgorithm(jose.ES384)
|
||||||
|
assert.Equal(t, jose.ES384, claims.SignatureAlg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAccessTokenClaims(t *testing.T) {
|
||||||
|
want := &AccessTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo"},
|
||||||
|
Expiration: 12345,
|
||||||
|
JWTID: "900",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NewAccessTokenClaims(
|
||||||
|
want.Issuer, want.Subject, nil,
|
||||||
|
want.Expiration.AsTime(), want.JWTID, "foo", time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
// test if the dynamic timestamps are around now,
|
||||||
|
// allowing for a delta of 1, just in case we flip on
|
||||||
|
// either side of a second boundry.
|
||||||
|
nowMinusSkew := NowTime() - 1
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.NotBefore), 1)
|
||||||
|
|
||||||
|
// Make equal not fail on dynamic timestamp
|
||||||
|
got.IssuedAt = 0
|
||||||
|
got.NotBefore = 0
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIDTokenClaims_GetAccessTokenHash(t *testing.T) {
|
||||||
|
assert.Equal(t, idTokenData.AccessTokenHash, idTokenData.GetAccessTokenHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIDTokenClaims_SetUserInfo(t *testing.T) {
|
||||||
|
want := IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Subject: userInfoData.Subject,
|
||||||
|
},
|
||||||
|
UserInfoProfile: userInfoData.UserInfoProfile,
|
||||||
|
UserInfoEmail: userInfoData.UserInfoEmail,
|
||||||
|
UserInfoPhone: userInfoData.UserInfoPhone,
|
||||||
|
Address: userInfoData.Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
var got IDTokenClaims
|
||||||
|
got.SetUserInfo(userInfoData)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIDTokenClaims(t *testing.T) {
|
||||||
|
want := &IDTokenClaims{
|
||||||
|
TokenClaims: TokenClaims{
|
||||||
|
Issuer: "zitadel",
|
||||||
|
Subject: "hello@me.com",
|
||||||
|
Audience: Audience{"foo", "just@me.com"},
|
||||||
|
Expiration: 12345,
|
||||||
|
AuthTime: 12000,
|
||||||
|
Nonce: "6969",
|
||||||
|
AuthenticationContextClassReference: "something",
|
||||||
|
AuthenticationMethodsReferences: []string{"some", "methods"},
|
||||||
|
AuthorizedParty: "just@me.com",
|
||||||
|
ClientID: "just@me.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := NewIDTokenClaims(
|
||||||
|
want.Issuer, want.Subject, want.Audience,
|
||||||
|
want.Expiration.AsTime(),
|
||||||
|
want.AuthTime.AsTime().Add(time.Second),
|
||||||
|
want.Nonce, want.AuthenticationContextClassReference,
|
||||||
|
want.AuthenticationMethodsReferences, want.AuthorizedParty,
|
||||||
|
time.Second,
|
||||||
|
)
|
||||||
|
|
||||||
|
// test if the dynamic timestamp is around now,
|
||||||
|
// allowing for a delta of 1, just in case we flip on
|
||||||
|
// either side of a second boundry.
|
||||||
|
nowMinusSkew := NowTime() - 1
|
||||||
|
assert.InDelta(t, int64(nowMinusSkew), int64(got.IssuedAt), 1)
|
||||||
|
|
||||||
|
// Make equal not fail on dynamic timestamp
|
||||||
|
got.IssuedAt = 0
|
||||||
|
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
|
@ -46,6 +46,39 @@ func (d *Display) UnmarshalText(text []byte) error {
|
||||||
|
|
||||||
type Gender string
|
type Gender string
|
||||||
|
|
||||||
|
type Locale struct {
|
||||||
|
tag language.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocale(tag language.Tag) *Locale {
|
||||||
|
return &Locale{tag: tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) Tag() language.Tag {
|
||||||
|
if l == nil {
|
||||||
|
return language.Und
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) String() string {
|
||||||
|
return l.Tag().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) MarshalJSON() ([]byte, error) {
|
||||||
|
tag := l.Tag()
|
||||||
|
if tag.IsRoot() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) UnmarshalJSON(data []byte) error {
|
||||||
|
return json.Unmarshal(data, &l.tag)
|
||||||
|
}
|
||||||
|
|
||||||
type Locales []language.Tag
|
type Locales []language.Tag
|
||||||
|
|
||||||
func (l *Locales) UnmarshalText(text []byte) error {
|
func (l *Locales) UnmarshalText(text []byte) error {
|
||||||
|
@ -137,19 +170,18 @@ func NewEncoder() *schema.Encoder {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
type Time time.Time
|
type Time int64
|
||||||
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
func (ts Time) AsTime() time.Time {
|
||||||
var i int64
|
return time.Unix(int64(ts), 0)
|
||||||
if err := json.Unmarshal(data, &i); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*t = Time(time.Unix(i, 0).UTC())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Time) MarshalJSON() ([]byte, error) {
|
func FromTime(tt time.Time) Time {
|
||||||
return json.Marshal(time.Time(*t).UTC().Unix())
|
return Time(tt.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NowTime() Time {
|
||||||
|
return FromTime(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestObject struct {
|
type RequestObject struct {
|
||||||
|
@ -162,5 +194,4 @@ func (r *RequestObject) GetIssuer() string {
|
||||||
return r.Issuer
|
return r.Issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
|
func (*RequestObject) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {}
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -111,6 +112,117 @@ func TestDisplay_UnmarshalText(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocale_Tag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want language.Tag
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: language.Afrikaans,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, tt.l.Tag())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want language.Tag
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: language.Und,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: language.Afrikaans,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want.String(), tt.l.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_MarshalJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
l *Locale
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
l: nil,
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "und",
|
||||||
|
l: NewLocale(language.Und),
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "language",
|
||||||
|
l: NewLocale(language.Afrikaans),
|
||||||
|
want: `"af"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := json.Marshal(tt.l)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocale_UnmarshalJSON(t *testing.T) {
|
||||||
|
type a struct {
|
||||||
|
Locale *Locale `json:"locale,omitempty"`
|
||||||
|
}
|
||||||
|
want := a{
|
||||||
|
Locale: NewLocale(language.Afrikaans),
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = `{"locale": "af"}`
|
||||||
|
var got a
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
json.Unmarshal([]byte(input), &got),
|
||||||
|
)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocales_UnmarshalText(t *testing.T) {
|
func TestLocales_UnmarshalText(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
text []byte
|
text []byte
|
||||||
|
|
|
@ -1,320 +1,73 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
// UserInfo implements OpenID Connect Core 1.0, section 5.1.
|
||||||
"encoding/json"
|
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims.
|
||||||
"fmt"
|
type UserInfo struct {
|
||||||
"time"
|
Subject string `json:"sub,omitempty"`
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserInfo interface {
|
|
||||||
GetSubject() string
|
|
||||||
UserInfoProfile
|
UserInfoProfile
|
||||||
UserInfoEmail
|
UserInfoEmail
|
||||||
UserInfoPhone
|
UserInfoPhone
|
||||||
GetAddress() UserInfoAddress
|
Address *UserInfoAddress `json:"address,omitempty"`
|
||||||
GetClaim(key string) interface{}
|
|
||||||
GetClaims() map[string]interface{}
|
Claims map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfoProfile interface {
|
func (u *UserInfo) AppendClaims(k string, v any) {
|
||||||
GetName() string
|
if u.Claims == nil {
|
||||||
GetGivenName() string
|
u.Claims = make(map[string]any)
|
||||||
GetFamilyName() string
|
}
|
||||||
GetMiddleName() string
|
|
||||||
GetNickname() string
|
u.Claims[k] = v
|
||||||
GetProfile() string
|
|
||||||
GetPicture() string
|
|
||||||
GetWebsite() string
|
|
||||||
GetGender() Gender
|
|
||||||
GetBirthdate() string
|
|
||||||
GetZoneinfo() string
|
|
||||||
GetLocale() language.Tag
|
|
||||||
GetPreferredUsername() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfoEmail interface {
|
// GetAddress is a safe getter that takes
|
||||||
GetEmail() string
|
// care of a possible nil value.
|
||||||
IsEmailVerified() bool
|
func (u *UserInfo) GetAddress() *UserInfoAddress {
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoPhone interface {
|
|
||||||
GetPhoneNumber() string
|
|
||||||
IsPhoneNumberVerified() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoAddress interface {
|
|
||||||
GetFormatted() string
|
|
||||||
GetStreetAddress() string
|
|
||||||
GetLocality() string
|
|
||||||
GetRegion() string
|
|
||||||
GetPostalCode() string
|
|
||||||
GetCountry() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoSetter interface {
|
|
||||||
UserInfo
|
|
||||||
SetSubject(sub string)
|
|
||||||
UserInfoProfileSetter
|
|
||||||
SetEmail(email string, verified bool)
|
|
||||||
SetPhone(phone string, verified bool)
|
|
||||||
SetAddress(address UserInfoAddress)
|
|
||||||
AppendClaims(key string, values interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoProfileSetter interface {
|
|
||||||
SetName(name string)
|
|
||||||
SetGivenName(name string)
|
|
||||||
SetFamilyName(name string)
|
|
||||||
SetMiddleName(name string)
|
|
||||||
SetNickname(name string)
|
|
||||||
SetUpdatedAt(date time.Time)
|
|
||||||
SetProfile(profile string)
|
|
||||||
SetPicture(profile string)
|
|
||||||
SetWebsite(website string)
|
|
||||||
SetGender(gender Gender)
|
|
||||||
SetBirthdate(birthdate string)
|
|
||||||
SetZoneinfo(zoneInfo string)
|
|
||||||
SetLocale(locale language.Tag)
|
|
||||||
SetPreferredUsername(name string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserInfo() UserInfoSetter {
|
|
||||||
return &userinfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type userinfo struct {
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
userInfoProfile
|
|
||||||
userInfoEmail
|
|
||||||
userInfoPhone
|
|
||||||
Address UserInfoAddress `json:"address,omitempty"`
|
|
||||||
|
|
||||||
claims map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetSubject() string {
|
|
||||||
return u.Subject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetName() string {
|
|
||||||
return u.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetGivenName() string {
|
|
||||||
return u.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetFamilyName() string {
|
|
||||||
return u.FamilyName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetMiddleName() string {
|
|
||||||
return u.MiddleName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetNickname() string {
|
|
||||||
return u.Nickname
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetProfile() string {
|
|
||||||
return u.Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPicture() string {
|
|
||||||
return u.Picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetWebsite() string {
|
|
||||||
return u.Website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetGender() Gender {
|
|
||||||
return u.Gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetBirthdate() string {
|
|
||||||
return u.Birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetZoneinfo() string {
|
|
||||||
return u.Zoneinfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetLocale() language.Tag {
|
|
||||||
return u.Locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPreferredUsername() string {
|
|
||||||
return u.PreferredUsername
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetEmail() string {
|
|
||||||
return u.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) IsEmailVerified() bool {
|
|
||||||
return bool(u.EmailVerified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetPhoneNumber() string {
|
|
||||||
return u.PhoneNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) IsPhoneNumberVerified() bool {
|
|
||||||
return u.PhoneNumberVerified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) GetAddress() UserInfoAddress {
|
|
||||||
if u.Address == nil {
|
if u.Address == nil {
|
||||||
return &userInfoAddress{}
|
return new(UserInfoAddress)
|
||||||
}
|
}
|
||||||
return u.Address
|
return u.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) GetClaim(key string) interface{} {
|
type uiAlias UserInfo
|
||||||
return u.claims[key]
|
|
||||||
|
func (u *UserInfo) MarshalJSON() ([]byte, error) {
|
||||||
|
return mergeAndMarshalClaims((*uiAlias)(u), u.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) GetClaims() map[string]interface{} {
|
func (u *UserInfo) UnmarshalJSON(data []byte) error {
|
||||||
return u.claims
|
return unmarshalJSONMulti(data, (*uiAlias)(u), &u.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) SetSubject(sub string) {
|
type UserInfoProfile struct {
|
||||||
u.Subject = sub
|
Name string `json:"name,omitempty"`
|
||||||
|
GivenName string `json:"given_name,omitempty"`
|
||||||
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
|
MiddleName string `json:"middle_name,omitempty"`
|
||||||
|
Nickname string `json:"nickname,omitempty"`
|
||||||
|
Profile string `json:"profile,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
Website string `json:"website,omitempty"`
|
||||||
|
Gender Gender `json:"gender,omitempty"`
|
||||||
|
Birthdate string `json:"birthdate,omitempty"`
|
||||||
|
Zoneinfo string `json:"zoneinfo,omitempty"`
|
||||||
|
Locale *Locale `json:"locale,omitempty"`
|
||||||
|
UpdatedAt Time `json:"updated_at,omitempty"`
|
||||||
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *userinfo) SetName(name string) {
|
type UserInfoEmail struct {
|
||||||
u.Name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetGivenName(name string) {
|
|
||||||
u.GivenName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetFamilyName(name string) {
|
|
||||||
u.FamilyName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetMiddleName(name string) {
|
|
||||||
u.MiddleName = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetNickname(name string) {
|
|
||||||
u.Nickname = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetUpdatedAt(date time.Time) {
|
|
||||||
u.UpdatedAt = Time(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetProfile(profile string) {
|
|
||||||
u.Profile = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPicture(picture string) {
|
|
||||||
u.Picture = picture
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetWebsite(website string) {
|
|
||||||
u.Website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetGender(gender Gender) {
|
|
||||||
u.Gender = gender
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetBirthdate(birthdate string) {
|
|
||||||
u.Birthdate = birthdate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetZoneinfo(zoneInfo string) {
|
|
||||||
u.Zoneinfo = zoneInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetLocale(locale language.Tag) {
|
|
||||||
u.Locale = locale
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPreferredUsername(name string) {
|
|
||||||
u.PreferredUsername = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetEmail(email string, verified bool) {
|
|
||||||
u.Email = email
|
|
||||||
u.EmailVerified = boolString(verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetPhone(phone string, verified bool) {
|
|
||||||
u.PhoneNumber = phone
|
|
||||||
u.PhoneNumberVerified = verified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) SetAddress(address UserInfoAddress) {
|
|
||||||
u.Address = address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) AppendClaims(key string, value interface{}) {
|
|
||||||
if u.claims == nil {
|
|
||||||
u.claims = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
u.claims[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetFormatted() string {
|
|
||||||
return u.Formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetStreetAddress() string {
|
|
||||||
return u.StreetAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetLocality() string {
|
|
||||||
return u.Locality
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetRegion() string {
|
|
||||||
return u.Region
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetPostalCode() string {
|
|
||||||
return u.PostalCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userInfoAddress) GetCountry() string {
|
|
||||||
return u.Country
|
|
||||||
}
|
|
||||||
|
|
||||||
type userInfoProfile struct {
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
GivenName string `json:"given_name,omitempty"`
|
|
||||||
FamilyName string `json:"family_name,omitempty"`
|
|
||||||
MiddleName string `json:"middle_name,omitempty"`
|
|
||||||
Nickname string `json:"nickname,omitempty"`
|
|
||||||
Profile string `json:"profile,omitempty"`
|
|
||||||
Picture string `json:"picture,omitempty"`
|
|
||||||
Website string `json:"website,omitempty"`
|
|
||||||
Gender Gender `json:"gender,omitempty"`
|
|
||||||
Birthdate string `json:"birthdate,omitempty"`
|
|
||||||
Zoneinfo string `json:"zoneinfo,omitempty"`
|
|
||||||
Locale language.Tag `json:"locale,omitempty"`
|
|
||||||
UpdatedAt Time `json:"updated_at,omitempty"`
|
|
||||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userInfoEmail struct {
|
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
|
||||||
// Handle providers that return email_verified as a string
|
// Handle providers that return email_verified as a string
|
||||||
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁
|
// https://forums.aws.amazon.com/thread.jspa?messageID=949441󧳁
|
||||||
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
// https://discuss.elastic.co/t/openid-error-after-authenticating-against-aws-cognito/206018/11
|
||||||
EmailVerified boolString `json:"email_verified,omitempty"`
|
EmailVerified Bool `json:"email_verified,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type boolString bool
|
type Bool bool
|
||||||
|
|
||||||
func (bs *boolString) UnmarshalJSON(data []byte) error {
|
func (bs *Bool) UnmarshalJSON(data []byte) error {
|
||||||
if string(data) == "true" || string(data) == `"true"` {
|
if string(data) == "true" || string(data) == `"true"` {
|
||||||
*bs = true
|
*bs = true
|
||||||
}
|
}
|
||||||
|
@ -322,12 +75,12 @@ func (bs *boolString) UnmarshalJSON(data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userInfoPhone struct {
|
type UserInfoPhone struct {
|
||||||
PhoneNumber string `json:"phone_number,omitempty"`
|
PhoneNumber string `json:"phone_number,omitempty"`
|
||||||
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
PhoneNumberVerified bool `json:"phone_number_verified,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userInfoAddress struct {
|
type UserInfoAddress struct {
|
||||||
Formatted string `json:"formatted,omitempty"`
|
Formatted string `json:"formatted,omitempty"`
|
||||||
StreetAddress string `json:"street_address,omitempty"`
|
StreetAddress string `json:"street_address,omitempty"`
|
||||||
Locality string `json:"locality,omitempty"`
|
Locality string `json:"locality,omitempty"`
|
||||||
|
@ -336,76 +89,6 @@ type userInfoAddress struct {
|
||||||
Country string `json:"country,omitempty"`
|
Country string `json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserInfoAddress(streetAddress, locality, region, postalCode, country, formatted string) UserInfoAddress {
|
|
||||||
return &userInfoAddress{
|
|
||||||
StreetAddress: streetAddress,
|
|
||||||
Locality: locality,
|
|
||||||
Region: region,
|
|
||||||
PostalCode: postalCode,
|
|
||||||
Country: country,
|
|
||||||
Formatted: formatted,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias userinfo
|
|
||||||
a := &struct {
|
|
||||||
*Alias
|
|
||||||
Locale interface{} `json:"locale,omitempty"`
|
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(u),
|
|
||||||
}
|
|
||||||
if !u.Locale.IsRoot() {
|
|
||||||
a.Locale = u.Locale
|
|
||||||
}
|
|
||||||
if !time.Time(u.UpdatedAt).IsZero() {
|
|
||||||
a.UpdatedAt = time.Time(u.UpdatedAt).Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(u.claims) == 0 {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &u.claims)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", u.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(u.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userinfo) UnmarshalJSON(data []byte) error {
|
|
||||||
type Alias userinfo
|
|
||||||
a := &struct {
|
|
||||||
Address *userInfoAddress `json:"address,omitempty"`
|
|
||||||
*Alias
|
|
||||||
UpdatedAt int64 `json:"update_at,omitempty"`
|
|
||||||
}{
|
|
||||||
Alias: (*Alias)(u),
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Address != nil {
|
|
||||||
u.Address = a.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
u.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &u.claims); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInfoRequest struct {
|
type UserInfoRequest struct {
|
||||||
AccessToken string `schema:"access_token"`
|
AccessToken string `schema:"access_token"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,54 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestUserInfo_AppendClaims(t *testing.T) {
|
||||||
|
u := new(UserInfo)
|
||||||
|
u.AppendClaims("a", "b")
|
||||||
|
want := map[string]any{"a": "b"}
|
||||||
|
assert.Equal(t, want, u.Claims)
|
||||||
|
|
||||||
|
u.AppendClaims("d", "e")
|
||||||
|
want["d"] = "e"
|
||||||
|
assert.Equal(t, want, u.Claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserInfo_GetAddress(t *testing.T) {
|
||||||
|
// nil address
|
||||||
|
u := new(UserInfo)
|
||||||
|
assert.Equal(t, &UserInfoAddress{}, u.GetAddress())
|
||||||
|
|
||||||
|
u.Address = &UserInfoAddress{PostalCode: "1234"}
|
||||||
|
assert.Equal(t, u.Address, u.GetAddress())
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserInfoMarshal(t *testing.T) {
|
func TestUserInfoMarshal(t *testing.T) {
|
||||||
userinfo := NewUserInfo()
|
userinfo := &UserInfo{
|
||||||
userinfo.SetSubject("test")
|
Subject: "test",
|
||||||
userinfo.SetAddress(NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", ""))
|
Address: &UserInfoAddress{
|
||||||
userinfo.SetEmail("test", true)
|
StreetAddress: "Test 789\nPostfach 2",
|
||||||
userinfo.SetPhone("0791234567", true)
|
},
|
||||||
userinfo.SetName("Test")
|
UserInfoEmail: UserInfoEmail{
|
||||||
userinfo.AppendClaims("private_claim", "test")
|
Email: "test",
|
||||||
|
EmailVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoPhone: UserInfoPhone{
|
||||||
|
PhoneNumber: "0791234567",
|
||||||
|
PhoneNumberVerified: true,
|
||||||
|
},
|
||||||
|
UserInfoProfile: UserInfoProfile{
|
||||||
|
Name: "Test",
|
||||||
|
},
|
||||||
|
Claims: map[string]any{"private_claim": "test"},
|
||||||
|
}
|
||||||
|
|
||||||
marshal, err := json.Marshal(userinfo)
|
marshal, err := json.Marshal(userinfo)
|
||||||
out := NewUserInfo()
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
out := new(UserInfo)
|
||||||
assert.NoError(t, json.Unmarshal(marshal, out))
|
assert.NoError(t, json.Unmarshal(marshal, out))
|
||||||
assert.Equal(t, userinfo.GetAddress(), out.GetAddress())
|
assert.Equal(t, userinfo, out)
|
||||||
expected, err := json.Marshal(out)
|
expected, err := json.Marshal(out)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expected, marshal)
|
assert.Equal(t, expected, marshal)
|
||||||
}
|
}
|
||||||
|
@ -29,91 +62,55 @@ func TestUserInfoMarshal(t *testing.T) {
|
||||||
func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) {
|
func TestUserInfoEmailVerifiedUnmarshal(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json bool true", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json bool true", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": true}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json string true", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json string true", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "true"}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: true,
|
EmailVerified: true,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json bool false", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json bool false", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": false}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarsha email_verified from json string false", func(t *testing.T) {
|
t.Run("unmarshal email_verified from json string false", func(t *testing.T) {
|
||||||
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`)
|
jsonBool := []byte(`{"email": "my@email.com", "email_verified": "false"}`)
|
||||||
|
|
||||||
var uie userInfoEmail
|
var uie UserInfoEmail
|
||||||
|
|
||||||
err := json.Unmarshal(jsonBool, &uie)
|
err := json.Unmarshal(jsonBool, &uie)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, userInfoEmail{
|
assert.Equal(t, UserInfoEmail{
|
||||||
Email: "my@email.com",
|
Email: "my@email.com",
|
||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
}, uie)
|
}, uie)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// issue 203 test case.
|
|
||||||
func Test_userinfo_GetAddress_issue_203(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
data string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "with address",
|
|
||||||
data: `{"address":{"street_address":"Test 789\nPostfach 2"},"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "without address",
|
|
||||||
data: `{"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "null address",
|
|
||||||
data: `{"address":null,"email":"test","email_verified":true,"name":"Test","phone_number":"0791234567","phone_number_verified":true,"private_claim":"test","sub":"test"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
info := &userinfo{}
|
|
||||||
err := json.Unmarshal([]byte(tt.data), info)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
info.GetAddress().GetCountry() //<- used to panic
|
|
||||||
|
|
||||||
// now shortly assure that a marshalling still produces the same as was parsed into the struct
|
|
||||||
marshal, err := json.Marshal(info)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.data, string(marshal))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
49
pkg/oidc/util.go
Normal file
49
pkg/oidc/util.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mergeAndMarshalClaims merges registered and the custom
|
||||||
|
// claims map into a single JSON object.
|
||||||
|
// Registered fields overwrite custom claims.
|
||||||
|
func mergeAndMarshalClaims(registered any, claims map[string]any) ([]byte, error) {
|
||||||
|
// Use a buffer for memory re-use, instead off letting
|
||||||
|
// json allocate a new []byte for every step.
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Marshal the registered claims into JSON
|
||||||
|
if err := json.NewEncoder(buf).Encode(registered); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc registered claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(claims) > 0 {
|
||||||
|
// Merge JSON data into custom claims.
|
||||||
|
// The full-read action by the decoder resets the buffer
|
||||||
|
// to zero len, while retaining underlaying cap.
|
||||||
|
if err := json.NewDecoder(buf).Decode(&claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc registered claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal the final result.
|
||||||
|
if err := json.NewEncoder(buf).Encode(claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("oidc custom claims: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalJSONMulti unmarshals the same JSON data into multiple destinations.
|
||||||
|
// Each destination must be a pointer, as per json.Unmarshal rules.
|
||||||
|
// Returns on the first error and destinations may be partly filled with data.
|
||||||
|
func unmarshalJSONMulti(data []byte, destinations ...any) error {
|
||||||
|
for _, dst := range destinations {
|
||||||
|
if err := json.Unmarshal(data, dst); err != nil {
|
||||||
|
return fmt.Errorf("oidc: %w into %T", err, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
147
pkg/oidc/util_test.go
Normal file
147
pkg/oidc/util_test.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonErrorTest struct{}
|
||||||
|
|
||||||
|
func (jsonErrorTest) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergeAndMarshalClaims(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
registered any
|
||||||
|
claims map[string]any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "encoder error",
|
||||||
|
args: args{
|
||||||
|
registered: jsonErrorTest{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no claims",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with claims",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
claims: map[string]any{
|
||||||
|
"bar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registered overwrites custom",
|
||||||
|
args: args{
|
||||||
|
registered: struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
claims: map[string]any{
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "{\"foo\":\"bar\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := mergeAndMarshalClaims(tt.args.registered, tt.args.claims)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_unmarshalJSONMulti(t *testing.T) {
|
||||||
|
type dst struct {
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
data string
|
||||||
|
destinations []any
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []any
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "error",
|
||||||
|
args: args{
|
||||||
|
data: "~!~~",
|
||||||
|
destinations: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
data: "{\"bar\":\"foo\",\"foo\":\"bar\"}\n",
|
||||||
|
destinations: []any{
|
||||||
|
&dst{},
|
||||||
|
&map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []any{
|
||||||
|
&dst{Foo: "bar"},
|
||||||
|
&map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := unmarshalJSONMulti([]byte(tt.args.data), tt.args.destinations...)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, tt.args.destinations)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,12 @@ type ClaimsSignature interface {
|
||||||
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
|
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IDClaims interface {
|
||||||
|
Claims
|
||||||
|
GetSignatureAlgorithm() jose.SignatureAlgorithm
|
||||||
|
GetAccessTokenHash() string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrParse = errors.New("parsing of request failed")
|
ErrParse = errors.New("parsing of request failed")
|
||||||
ErrIssuerInvalid = errors.New("issuer does not match")
|
ErrIssuerInvalid = errors.New("issuer does not match")
|
||||||
|
|
|
@ -371,7 +371,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
|
||||||
if idTokenHint == "" {
|
if idTokenHint == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
claims, err := VerifyIDTokenHint(ctx, idTokenHint, verifier)
|
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, idTokenHint, verifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
|
return "", oidc.ErrLoginRequired().WithDescription("The id_token_hint is invalid. " +
|
||||||
"If you have any questions, you may contact the administrator of the application.")
|
"If you have any questions, you may contact the administrator of the application.")
|
||||||
|
|
|
@ -263,7 +263,7 @@ func (mr *MockStorageMockRecorder) SaveAuthCode(arg0, arg1, arg2 interface{}) *g
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetIntrospectionFromToken mocks base method.
|
// SetIntrospectionFromToken mocks base method.
|
||||||
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
|
func (m *MockStorage) SetIntrospectionFromToken(arg0 context.Context, arg1 *oidc.IntrospectionResponse, arg2, arg3, arg4 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetIntrospectionFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
@ -277,7 +277,7 @@ func (mr *MockStorageMockRecorder) SetIntrospectionFromToken(arg0, arg1, arg2, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromScopes mocks base method.
|
// SetUserinfoFromScopes mocks base method.
|
||||||
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3 string, arg4 []string) error {
|
func (m *MockStorage) SetUserinfoFromScopes(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3 string, arg4 []string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetUserinfoFromScopes", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
@ -291,7 +291,7 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromScopes(arg0, arg1, arg2, arg3,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetUserinfoFromToken mocks base method.
|
// SetUserinfoFromToken mocks base method.
|
||||||
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 oidc.UserInfoSetter, arg2, arg3, arg4 string) error {
|
func (m *MockStorage) SetUserinfoFromToken(arg0 context.Context, arg1 *oidc.UserInfo, arg2, arg3, arg4 string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "SetUserinfoFromToken", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
|
|
|
@ -59,7 +59,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
|
||||||
RedirectURI: ender.DefaultLogoutRedirectURI(),
|
RedirectURI: ender.DefaultLogoutRedirectURI(),
|
||||||
}
|
}
|
||||||
if req.IdTokenHint != "" {
|
if req.IdTokenHint != "" {
|
||||||
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
|
claims, err := VerifyIDTokenHint[*oidc.TokenClaims](ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
|
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ type TokenExchangeStorage interface {
|
||||||
|
|
||||||
// SetUserinfoFromTokenExchangeRequest will be called during id token creation.
|
// SetUserinfoFromTokenExchangeRequest will be called during id token creation.
|
||||||
// Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc.
|
// Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc.
|
||||||
SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo oidc.UserInfoSetter, request TokenExchangeRequest) error
|
SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request TokenExchangeRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens
|
// TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens
|
||||||
|
@ -111,9 +111,9 @@ var ErrInvalidRefreshToken = errors.New("invalid_refresh_token")
|
||||||
type OPStorage interface {
|
type OPStorage interface {
|
||||||
GetClientByClientID(ctx context.Context, clientID string) (Client, error)
|
GetClientByClientID(ctx context.Context, clientID string) (Client, error)
|
||||||
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
|
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
|
||||||
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error
|
SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error
|
||||||
SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error
|
SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error
|
||||||
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error
|
SetIntrospectionFromToken(ctx context.Context, userinfo *oidc.IntrospectionResponse, tokenID, subject, clientID string) error
|
||||||
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
|
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
|
||||||
GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
|
GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
|
||||||
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
|
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
|
||||||
|
|
|
@ -129,7 +129,7 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
claims.SetPrivateClaims(privateClaims)
|
claims.Claims = privateClaims
|
||||||
}
|
}
|
||||||
signingKey, err := storage.SigningKey(ctx)
|
signingKey, err := storage.SigningKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -169,7 +169,7 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
claims.SetAccessTokenHash(atHash)
|
claims.AccessTokenHash = atHash
|
||||||
if !client.IDTokenUserinfoClaimsAssertion() {
|
if !client.IDTokenUserinfoClaimsAssertion() {
|
||||||
scopes = removeUserinfoScopes(scopes)
|
scopes = removeUserinfoScopes(scopes)
|
||||||
}
|
}
|
||||||
|
@ -178,26 +178,26 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v
|
||||||
tokenExchangeRequest, okReq := request.(TokenExchangeRequest)
|
tokenExchangeRequest, okReq := request.(TokenExchangeRequest)
|
||||||
teStorage, okStorage := storage.(TokenExchangeStorage)
|
teStorage, okStorage := storage.(TokenExchangeStorage)
|
||||||
if okReq && okStorage {
|
if okReq && okStorage {
|
||||||
userInfo := oidc.NewUserInfo()
|
userInfo := new(oidc.UserInfo)
|
||||||
err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest)
|
err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
claims.SetUserinfo(userInfo)
|
claims.SetUserInfo(userInfo)
|
||||||
} else if len(scopes) > 0 {
|
} else if len(scopes) > 0 {
|
||||||
userInfo := oidc.NewUserInfo()
|
userInfo := new(oidc.UserInfo)
|
||||||
err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes)
|
err := storage.SetUserinfoFromScopes(ctx, userInfo, request.GetSubject(), request.GetClientID(), scopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
claims.SetUserinfo(userInfo)
|
claims.SetUserInfo(userInfo)
|
||||||
}
|
}
|
||||||
if code != "" {
|
if code != "" {
|
||||||
codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm())
|
codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
claims.SetCodeHash(codeHash)
|
claims.CodeHash = codeHash
|
||||||
}
|
}
|
||||||
signer, err := SignerFromKey(signingKey)
|
signer, err := SignerFromKey(signingKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -280,9 +280,9 @@ func GetTokenIDAndSubjectFromToken(
|
||||||
) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) {
|
) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) {
|
||||||
switch tokenType {
|
switch tokenType {
|
||||||
case oidc.AccessTokenType:
|
case oidc.AccessTokenType:
|
||||||
var accessTokenClaims oidc.AccessTokenClaims
|
var accessTokenClaims *oidc.AccessTokenClaims
|
||||||
tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token)
|
tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token)
|
||||||
claims = accessTokenClaims.GetClaims()
|
claims = accessTokenClaims.Claims
|
||||||
case oidc.RefreshTokenType:
|
case oidc.RefreshTokenType:
|
||||||
refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token)
|
refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -291,12 +291,12 @@ func GetTokenIDAndSubjectFromToken(
|
||||||
|
|
||||||
tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true
|
tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true
|
||||||
case oidc.IDTokenType:
|
case oidc.IDTokenType:
|
||||||
idTokenClaims, err := VerifyIDTokenHint(ctx, token, exchanger.IDTokenHintVerifier(ctx))
|
idTokenClaims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, token, exchanger.IDTokenHintVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenIDOrToken, subject, claims, ok = token, idTokenClaims.GetSubject(), idTokenClaims.GetClaims(), true
|
tokenIDOrToken, subject, claims, ok = token, idTokenClaims.Subject, idTokenClaims.Claims, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -380,7 +380,7 @@ func CreateTokenExchangeResponse(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, oidc.AccessTokenClaims, bool) {
|
func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, *oidc.AccessTokenClaims, bool) {
|
||||||
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
splitToken := strings.Split(tokenIDSubject, ":")
|
splitToken := strings.Split(tokenIDSubject, ":")
|
||||||
|
@ -390,10 +390,10 @@ func getTokenIDAndClaims(ctx context.Context, userinfoProvider UserinfoProvider,
|
||||||
|
|
||||||
return splitToken[0], splitToken[1], nil, true
|
return splitToken[0], splitToken[1], nil, true
|
||||||
}
|
}
|
||||||
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, false
|
return "", "", nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), accessTokenClaims, true
|
return accessTokenClaims.JWTID, accessTokenClaims.Subject, accessTokenClaims, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
|
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
|
||||||
response := oidc.NewIntrospectionResponse()
|
response := new(oidc.IntrospectionResponse)
|
||||||
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
|
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
@ -44,7 +44,7 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto
|
||||||
httphelper.MarshalJSON(w, response)
|
httphelper.MarshalJSON(w, response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.SetActive(true)
|
response.Active = true
|
||||||
httphelper.MarshalJSON(w, response)
|
httphelper.MarshalJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -151,9 +151,9 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use
|
||||||
}
|
}
|
||||||
return splitToken[0], splitToken[1], true
|
return splitToken[0], splitToken[1], true
|
||||||
}
|
}
|
||||||
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true
|
return accessTokenClaims.JWTID, accessTokenClaims.Subject, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
|
||||||
http.Error(w, "access token invalid", http.StatusUnauthorized)
|
http.Error(w, "access token invalid", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := oidc.NewUserInfo()
|
info := new(oidc.UserInfo)
|
||||||
err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin"))
|
err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httphelper.MarshalJSONWithStatus(w, err, http.StatusForbidden)
|
httphelper.MarshalJSONWithStatus(w, err, http.StatusForbidden)
|
||||||
|
@ -81,9 +81,9 @@ func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider
|
||||||
}
|
}
|
||||||
return splitToken[0], splitToken[1], true
|
return splitToken[0], splitToken[1], true
|
||||||
}
|
}
|
||||||
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
return accessTokenClaims.GetTokenID(), accessTokenClaims.GetSubject(), true
|
return accessTokenClaims.JWTID, accessTokenClaims.Subject, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,6 @@ type accessTokenVerifier struct {
|
||||||
maxAgeIAT time.Duration
|
maxAgeIAT time.Duration
|
||||||
offset time.Duration
|
offset time.Duration
|
||||||
supportedSignAlgs []string
|
supportedSignAlgs []string
|
||||||
maxAge time.Duration
|
|
||||||
acr oidc.ACRVerifier
|
|
||||||
keySet oidc.KeySet
|
keySet oidc.KeySet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,29 +65,29 @@ func NewAccessTokenVerifier(issuer string, keySet oidc.KeySet, opts ...AccessTok
|
||||||
return verifier
|
return verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyAccessToken validates the access token (issuer, signature and expiration)
|
// VerifyAccessToken validates the access token (issuer, signature and expiration).
|
||||||
func VerifyAccessToken(ctx context.Context, token string, v AccessTokenVerifier) (oidc.AccessTokenClaims, error) {
|
func VerifyAccessToken[C oidc.Claims](ctx context.Context, token string, v AccessTokenVerifier) (claims C, err error) {
|
||||||
claims := oidc.EmptyAccessTokenClaims()
|
var nilClaims C
|
||||||
|
|
||||||
decrypted, err := oidc.DecryptToken(token)
|
decrypted, err := oidc.DecryptToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
payload, err := oidc.ParseToken(decrypted, claims)
|
payload, err := oidc.ParseToken(decrypted, &claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, nil
|
||||||
|
|
70
pkg/op/verifier_access_token_example_test.go
Normal file
70
pkg/op/verifier_access_token_example_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package op_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyCustomClaims extends the TokenClaims base,
|
||||||
|
// so it implements the oidc.Claims interface.
|
||||||
|
// Instead of carrying a map, we add needed fields// to the struct for type safe access.
|
||||||
|
type MyCustomClaims struct {
|
||||||
|
oidc.TokenClaims
|
||||||
|
NotBefore oidc.Time `json:"nbf,omitempty"`
|
||||||
|
CodeHash string `json:"c_hash,omitempty"`
|
||||||
|
SessionID string `json:"sid,omitempty"`
|
||||||
|
Scopes []string `json:"scope,omitempty"`
|
||||||
|
AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
|
||||||
|
Foo string `json:"foo,omitempty"`
|
||||||
|
Bar *Nested `json:"bar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested struct types are also possible.
|
||||||
|
type Nested struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
accessToken carries the following claims. foo and bar are custom claims
|
||||||
|
|
||||||
|
{
|
||||||
|
"aud": [
|
||||||
|
"unit",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"bar": {
|
||||||
|
"count": 22,
|
||||||
|
"tags": [
|
||||||
|
"some",
|
||||||
|
"tags"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exp": 4802234675,
|
||||||
|
"foo": "Hello, World!",
|
||||||
|
"iat": 1678097014,
|
||||||
|
"iss": "local.com",
|
||||||
|
"jti": "9876",
|
||||||
|
"nbf": 1678097014,
|
||||||
|
"sub": "tim@local.com"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const accessToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOlsidW5pdCIsInRlc3QiXSwiYmFyIjp7ImNvdW50IjoyMiwidGFncyI6WyJzb21lIiwidGFncyJdfSwiZXhwIjo0ODAyMjM0Njc1LCJmb28iOiJIZWxsbywgV29ybGQhIiwiaWF0IjoxNjc4MDk3MDE0LCJpc3MiOiJsb2NhbC5jb20iLCJqdGkiOiI5ODc2IiwibmJmIjoxNjc4MDk3MDE0LCJzdWIiOiJ0aW1AbG9jYWwuY29tIn0.OUgk-B7OXjYlYFj-nogqSDJiQE19tPrbzqUHEAjcEiJkaWo6-IpGVfDiGKm-TxjXQsNScxpaY0Pg3XIh1xK6TgtfYtoLQm-5RYw_mXgb9xqZB2VgPs6nNEYFUDM513MOU0EBc0QMyqAEGzW-HiSPAb4ugCvkLtM1yo11Xyy6vksAdZNs_mJDT4X3vFXnr0jk0ugnAW6fTN3_voC0F_9HQUAkmd750OIxkAHxAMvEPQcpbLHenVvX_Q0QMrzClVrxehn5TVMfmkYYg7ocr876Bq9xQGPNHAcrwvVIJqdg5uMUA38L3HC2BEueG6furZGvc7-qDWAT1VR9liM5ieKpPg`
|
||||||
|
|
||||||
|
func ExampleVerifyAccessToken_customClaims() {
|
||||||
|
v := op.NewAccessTokenVerifier("local.com", tu.KeySet{})
|
||||||
|
|
||||||
|
// VerifyAccessToken can be called with the *MyCustomClaims.
|
||||||
|
claims, err := op.VerifyAccessToken[*MyCustomClaims](context.TODO(), accessToken, v)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we have typesafe access to the custom claims
|
||||||
|
fmt.Println(claims.Foo, claims.Bar.Count, claims.Bar.Tags)
|
||||||
|
// Output: Hello, World! 22 [some tags]
|
||||||
|
}
|
126
pkg/op/verifier_access_token_test.go
Normal file
126
pkg/op/verifier_access_token_test.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewAccessTokenVerifier(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
issuer string
|
||||||
|
keySet oidc.KeySet
|
||||||
|
opts []AccessTokenVerifierOpt
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want AccessTokenVerifier
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple",
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
},
|
||||||
|
want: &accessTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with signature algorithm",
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
opts: []AccessTokenVerifierOpt{
|
||||||
|
WithSupportedAccessTokenSigningAlgorithms("ABC", "DEF"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &accessTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := NewAccessTokenVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyAccessToken(t *testing.T) {
|
||||||
|
verifier := &accessTokenVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenClaims func() (string, *oidc.AccessTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
tokenClaims: tu.ValidAccessToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse err",
|
||||||
|
tokenClaims: func() (string, *oidc.AccessTokenClaims) { return "~~~~", nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signature",
|
||||||
|
tokenClaims: func() (string, *oidc.AccessTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong issuer",
|
||||||
|
tokenClaims: func() (string, *oidc.AccessTokenClaims) {
|
||||||
|
return tu.NewAccessToken(
|
||||||
|
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidJWTID, tu.ValidClientID,
|
||||||
|
tu.ValidSkew,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
tokenClaims: func() (string, *oidc.AccessTokenClaims) {
|
||||||
|
return tu.NewAccessToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidJWTID, tu.ValidClientID,
|
||||||
|
tu.ValidSkew,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
token, want := tt.tokenClaims()
|
||||||
|
|
||||||
|
got, err := VerifyAccessToken[*oidc.AccessTokenClaims](context.Background(), token, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,40 +74,40 @@ func NewIDTokenHintVerifier(issuer string, keySet oidc.KeySet, opts ...IDTokenHi
|
||||||
|
|
||||||
// VerifyIDTokenHint validates the id token according to
|
// VerifyIDTokenHint validates the id token according to
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||||
func VerifyIDTokenHint(ctx context.Context, token string, v IDTokenHintVerifier) (oidc.IDTokenClaims, error) {
|
func VerifyIDTokenHint[C oidc.Claims](ctx context.Context, token string, v IDTokenHintVerifier) (claims C, err error) {
|
||||||
claims := oidc.EmptyIDTokenClaims()
|
var nilClaims C
|
||||||
|
|
||||||
decrypted, err := oidc.DecryptToken(token)
|
decrypted, err := oidc.DecryptToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
payload, err := oidc.ParseToken(decrypted, claims)
|
payload, err := oidc.ParseToken(decrypted, &claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
if err := oidc.CheckIssuer(claims, v.Issuer()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
if err = oidc.CheckSignature(ctx, decrypted, payload, claims, v.SupportedSignAlgs(), v.KeySet()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
if err = oidc.CheckExpiration(claims, v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
if err = oidc.CheckIssuedAt(claims, v.MaxAgeIAT(), v.Offset()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
if err = oidc.CheckAuthorizationContextClassReference(claims, v.ACR()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
if err = oidc.CheckAuthTime(claims, v.MaxAge()); err != nil {
|
||||||
return nil, err
|
return nilClaims, err
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
161
pkg/op/verifier_id_token_hint_test.go
Normal file
161
pkg/op/verifier_id_token_hint_test.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package op
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
tu "github.com/zitadel/oidc/v2/internal/testutil"
|
||||||
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewIDTokenHintVerifier(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
issuer string
|
||||||
|
keySet oidc.KeySet
|
||||||
|
opts []IDTokenHintVerifierOpt
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want IDTokenHintVerifier
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple",
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
},
|
||||||
|
want: &idTokenHintVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with signature algorithm",
|
||||||
|
args: args{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
opts: []IDTokenHintVerifierOpt{
|
||||||
|
WithSupportedIDTokenHintSigningAlgorithms("ABC", "DEF"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &idTokenHintVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
supportedSignAlgs: []string{"ABC", "DEF"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := NewIDTokenHintVerifier(tt.args.issuer, tt.args.keySet, tt.args.opts...)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyIDTokenHint(t *testing.T) {
|
||||||
|
verifier := &idTokenHintVerifier{
|
||||||
|
issuer: tu.ValidIssuer,
|
||||||
|
maxAgeIAT: 2 * time.Minute,
|
||||||
|
offset: time.Second,
|
||||||
|
supportedSignAlgs: []string{string(tu.SignatureAlgorithm)},
|
||||||
|
maxAge: 2 * time.Minute,
|
||||||
|
acr: tu.ACRVerify,
|
||||||
|
keySet: tu.KeySet{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenClaims func() (string, *oidc.IDTokenClaims)
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
tokenClaims: tu.ValidIDToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parse err",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return "~~~~", nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signature",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) { return tu.InvalidSignatureToken, nil },
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong issuer",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
"foo", tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration.Add(-time.Hour), tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong IAT",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, -time.Hour, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong acr",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime, tu.ValidNonce,
|
||||||
|
"else", tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired auth",
|
||||||
|
tokenClaims: func() (string, *oidc.IDTokenClaims) {
|
||||||
|
return tu.NewIDToken(
|
||||||
|
tu.ValidIssuer, tu.ValidSubject, tu.ValidAudience,
|
||||||
|
tu.ValidExpiration, tu.ValidAuthTime.Add(-time.Hour), tu.ValidNonce,
|
||||||
|
tu.ValidACR, tu.ValidAMR, tu.ValidClientID, tu.ValidSkew, "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
token, want := tt.tokenClaims()
|
||||||
|
|
||||||
|
got, err := VerifyIDTokenHint[*oidc.IDTokenClaims](context.Background(), token, verifier)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, got, want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue