diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9509826..2abef36 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/README.md b/README.md
index 31287e9..7b9bf22 100644
--- a/README.md
+++ b/README.md
@@ -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: |
diff --git a/example/client/api/api.go b/example/client/api/api.go
index c475354..8093b63 100644
--- a/example/client/api/api.go
+++ b/example/client/api/api.go
@@ -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
diff --git a/example/client/app/app.go b/example/client/app/app.go
index 97e8948..560ac02 100644
--- a/example/client/app/app.go
+++ b/example/client/app/app.go
@@ -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)
diff --git a/example/client/github/github.go b/example/client/github/github.go
index 57bb3ae..9cb813c 100644
--- a/example/client/github/github.go
+++ b/example/client/github/github.go
@@ -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))
diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go
index 2794783..ff7889e 100644
--- a/example/server/storage/storage.go
+++ b/example/server/storage/storage.go
@@ -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
diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go
index d424a89..6e5ee32 100644
--- a/example/server/storage/storage_dynamic.go
+++ b/example/server/storage/storage_dynamic.go
@@ -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
diff --git a/go.mod b/go.mod
index 2691e57..9ed1e8e 100644
--- a/go.mod
+++ b/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
+)
diff --git a/go.sum b/go.sum
index c73eb9d..1933228 100644
--- a/go.sum
+++ b/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=
diff --git a/internal/testutil/gen/gen.go b/internal/testutil/gen/gen.go
new file mode 100644
index 0000000..a9f5925
--- /dev/null
+++ b/internal/testutil/gen/gen.go
@@ -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)
+}
diff --git a/internal/testutil/token.go b/internal/testutil/token.go
new file mode 100644
index 0000000..121aa0b
--- /dev/null
+++ b/internal/testutil/token.go
@@ -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
+}
diff --git a/pkg/client/client.go b/pkg/client/client.go
index ebe1442..9eda973 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -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)
}
diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go
index f112b30..7f6ca62 100644
--- a/pkg/client/integration_test.go
+++ b/pkg/client/integration_test.go
@@ -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()
diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go
index 936f319..91b200d 100644
--- a/pkg/client/rp/cli/cli.go
+++ b/pkg/client/rp/cli/cli.go
@@ -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 := "
Success!
"
msg = msg + "You are authenticated and can now return to the CLI.
"
diff --git a/pkg/client/rp/mock/generate.go b/pkg/client/rp/mock/generate.go
deleted file mode 100644
index 7db81ea..0000000
--- a/pkg/client/rp/mock/generate.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package mock
-
-//go:generate mockgen -package mock -destination ./verifier.mock.go github.com/zitadel/oidc/v2/pkg/client/rp IDTokenVerifier
diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go
deleted file mode 100644
index eac6a79..0000000
--- a/pkg/client/rp/mock/verifier.mock.go
+++ /dev/null
@@ -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))
-}
diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go
index 5bd3558..8aa7b1e 100644
--- a/pkg/client/rp/relying_party.go
+++ b/pkg/client/rp/relying_party.go
@@ -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
diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go
index f3db128..75d149b 100644
--- a/pkg/client/rp/verifier.go
+++ b/pkg/client/rp/verifier.go
@@ -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
}
}
diff --git a/pkg/client/rp/verifier_test.go b/pkg/client/rp/verifier_test.go
new file mode 100644
index 0000000..7588c1f
--- /dev/null
+++ b/pkg/client/rp/verifier_test.go
@@ -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)
+ })
+ }
+}
diff --git a/pkg/client/rp/verifier_tokens_example_test.go b/pkg/client/rp/verifier_tokens_example_test.go
new file mode 100644
index 0000000..c297efe
--- /dev/null
+++ b/pkg/client/rp/verifier_tokens_example_test.go
@@ -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]
+}
diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go
index 95a0121..4e0353c 100644
--- a/pkg/client/rs/resource_server.go
+++ b/pkg/client/rs/resource_server.go
@@ -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
}
diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go
index b7c220c..8313dc4 100644
--- a/pkg/oidc/introspection.go
+++ b/pkg/oidc/introspection.go
@@ -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)
}
diff --git a/pkg/oidc/introspection_test.go b/pkg/oidc/introspection_test.go
new file mode 100644
index 0000000..bd49894
--- /dev/null
+++ b/pkg/oidc/introspection_test.go
@@ -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"}`)
+}
diff --git a/pkg/oidc/regression_assert_test.go b/pkg/oidc/regression_assert_test.go
new file mode 100644
index 0000000..5e9fb3d
--- /dev/null
+++ b/pkg/oidc/regression_assert_test.go
@@ -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))
+ })
+ }
+}
diff --git a/pkg/oidc/regression_create_test.go b/pkg/oidc/regression_create_test.go
new file mode 100644
index 0000000..809fe60
--- /dev/null
+++ b/pkg/oidc/regression_create_test.go
@@ -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)
+ }
+}
diff --git a/pkg/oidc/regression_data/oidc.AccessTokenClaims.json b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json
new file mode 100644
index 0000000..e4f7808
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json
@@ -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"
+}
diff --git a/pkg/oidc/regression_data/oidc.IDTokenClaims.json b/pkg/oidc/regression_data/oidc.IDTokenClaims.json
new file mode 100644
index 0000000..af503fb
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.IDTokenClaims.json
@@ -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"
+}
diff --git a/pkg/oidc/regression_data/oidc.IntrospectionResponse.json b/pkg/oidc/regression_data/oidc.IntrospectionResponse.json
new file mode 100644
index 0000000..e0c21a2
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.IntrospectionResponse.json
@@ -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"
+}
diff --git a/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json b/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
new file mode 100644
index 0000000..4ece780
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.JWTProfileAssertionClaims.json
@@ -0,0 +1,11 @@
+{
+ "aud": [
+ "foo",
+ "bar"
+ ],
+ "exp": 12345,
+ "foo": "bar",
+ "iat": 12000,
+ "iss": "zitadel",
+ "sub": "hello@me.com"
+}
diff --git a/pkg/oidc/regression_data/oidc.UserInfo.json b/pkg/oidc/regression_data/oidc.UserInfo.json
new file mode 100644
index 0000000..d7795e7
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.UserInfo.json
@@ -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"
+}
diff --git a/pkg/oidc/regression_test.go b/pkg/oidc/regression_test.go
new file mode 100644
index 0000000..5d33bb6
--- /dev/null
+++ b/pkg/oidc/regression_test.go
@@ -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/.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,
+}
diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go
index b538465..1ade913 100644
--- a/pkg/oidc/token.go
+++ b/pkg/oidc/token.go
@@ -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 {
diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go
index 78bd658..e63e0e5 100644
--- a/pkg/oidc/token_request.go
+++ b/pkg/oidc/token_request.go
@@ -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
diff --git a/pkg/oidc/token_test.go b/pkg/oidc/token_test.go
new file mode 100644
index 0000000..0d9874e
--- /dev/null
+++ b/pkg/oidc/token_test.go
@@ -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)
+}
diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go
index 21b6fba..415ab04 100644
--- a/pkg/oidc/types.go
+++ b/pkg/oidc/types.go
@@ -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) {}
diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go
index 74323da..c8e2801 100644
--- a/pkg/oidc/types_test.go
+++ b/pkg/oidc/types_test.go
@@ -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
diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go
index c8e34d6..caff58e 100644
--- a/pkg/oidc/userinfo.go
+++ b/pkg/oidc/userinfo.go
@@ -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"`
}
diff --git a/pkg/oidc/userinfo_test.go b/pkg/oidc/userinfo_test.go
index 319a2fd..faab4e3 100644
--- a/pkg/oidc/userinfo_test.go
+++ b/pkg/oidc/userinfo_test.go
@@ -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))
- })
- }
-}
diff --git a/pkg/oidc/util.go b/pkg/oidc/util.go
new file mode 100644
index 0000000..a89d75e
--- /dev/null
+++ b/pkg/oidc/util.go
@@ -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
+}
diff --git a/pkg/oidc/util_test.go b/pkg/oidc/util_test.go
new file mode 100644
index 0000000..6363d83
--- /dev/null
+++ b/pkg/oidc/util_test.go
@@ -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)
+ })
+ }
+}
diff --git a/pkg/oidc/verifier.go b/pkg/oidc/verifier.go
index 1757651..c4ee95e 100644
--- a/pkg/oidc/verifier.go
+++ b/pkg/oidc/verifier.go
@@ -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")
diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go
index b13f642..bd6aa95 100644
--- a/pkg/op/auth_request.go
+++ b/pkg/op/auth_request.go
@@ -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.")
diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go
index fc0c358..85afb2a 100644
--- a/pkg/op/mock/storage.mock.go
+++ b/pkg/op/mock/storage.mock.go
@@ -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)
diff --git a/pkg/op/session.go b/pkg/op/session.go
index e1cc595..3e5ec3c 100644
--- a/pkg/op/session.go
+++ b/pkg/op/session.go
@@ -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)
}
diff --git a/pkg/op/storage.go b/pkg/op/storage.go
index 8ba1946..5940bd9 100644
--- a/pkg/op/storage.go
+++ b/pkg/op/storage.go
@@ -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)
diff --git a/pkg/op/token.go b/pkg/op/token.go
index 3a35062..58568a7 100644
--- a/pkg/op/token.go
+++ b/pkg/op/token.go
@@ -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 {
diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go
index 6b918b1..055ff13 100644
--- a/pkg/op/token_exchange.go
+++ b/pkg/op/token_exchange.go
@@ -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
}
diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go
index e7ca7c4..8582388 100644
--- a/pkg/op/token_intospection.go
+++ b/pkg/op/token_intospection.go
@@ -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)
}
diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go
index 33978f5..58332c3 100644
--- a/pkg/op/token_revocation.go
+++ b/pkg/op/token_revocation.go
@@ -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
}
diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go
index cb8f0ae..21a0af4 100644
--- a/pkg/op/userinfo.go
+++ b/pkg/op/userinfo.go
@@ -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
}
diff --git a/pkg/op/verifier_access_token.go b/pkg/op/verifier_access_token.go
index 1d53adb..9a8b912 100644
--- a/pkg/op/verifier_access_token.go
+++ b/pkg/op/verifier_access_token.go
@@ -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
diff --git a/pkg/op/verifier_access_token_example_test.go b/pkg/op/verifier_access_token_example_test.go
new file mode 100644
index 0000000..effdd58
--- /dev/null
+++ b/pkg/op/verifier_access_token_example_test.go
@@ -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]
+}
diff --git a/pkg/op/verifier_access_token_test.go b/pkg/op/verifier_access_token_test.go
new file mode 100644
index 0000000..62c26a9
--- /dev/null
+++ b/pkg/op/verifier_access_token_test.go
@@ -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)
+ })
+ }
+}
diff --git a/pkg/op/verifier_id_token_hint.go b/pkg/op/verifier_id_token_hint.go
index 9320106..d906075 100644
--- a/pkg/op/verifier_id_token_hint.go
+++ b/pkg/op/verifier_id_token_hint.go
@@ -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
}
diff --git a/pkg/op/verifier_id_token_hint_test.go b/pkg/op/verifier_id_token_hint_test.go
new file mode 100644
index 0000000..f4d0b0c
--- /dev/null
+++ b/pkg/op/verifier_id_token_hint_test.go
@@ -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)
+ })
+ }
+}