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:
Tim Möhlmann 2023-03-10 16:31:22 +02:00 committed by GitHub
parent 4bd2b742f9
commit dea8bc96ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 2358 additions and 1516 deletions

View file

@ -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

View file

@ -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: |

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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=

View 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
View 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
}

View file

@ -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)
} }

View file

@ -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()

View file

@ -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>"

View file

@ -1,3 +0,0 @@
package mock
//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier

View file

@ -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))
}

View file

@ -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

View file

@ -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
} }
} }

View 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)
})
}
}

View 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]
}

View file

@ -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
} }

View file

@ -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
} }

View 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"}`)
}

View 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))
})
}
}

View 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)
}
}

View 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"
}

View 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"
}

View 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"
}

View file

@ -0,0 +1,11 @@
{
"aud": [
"foo",
"bar"
],
"exp": 12345,
"foo": "bar",
"iat": 12000,
"iss": "zitadel",
"sub": "hello@me.com"
}

View 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"
}

View 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,
}

View file

@ -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 {

View file

@ -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
View 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)
}

View file

@ -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) {}
}

View file

@ -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

View file

@ -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&#949441 // https://forums.aws.amazon.com/thread.jspa?messageID=949441&#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"`
} }

View file

@ -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
View 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
View 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)
})
}
}

View file

@ -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")

View file

@ -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.")

View file

@ -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)

View file

@ -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)
} }

View file

@ -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)

View file

@ -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 {

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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

View 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]
}

View 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)
})
}
}

View file

@ -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
} }

View 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)
})
}
}