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