diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 85ea2ca..d2bae79 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,10 +2,10 @@ name: "Code scanning - action"
on:
push:
- branches: [main, ]
+ branches: [main,next]
pull_request:
# The branches below must be a subset of the branches above
- branches: [main]
+ branches: [main,next]
schedule:
- cron: '0 11 * * 0'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d97d41a..2abef36 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,6 +3,7 @@ on:
push:
branches:
- main
+ - next
tags-ignore:
- '**'
pull_request:
@@ -15,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
@@ -23,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
@@ -31,7 +32,7 @@ jobs:
release:
runs-on: ubuntu-20.04
needs: [test]
- if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
+ if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
diff --git a/.releaserc.js b/.releaserc.js
index 6500ace..e8eea8e 100644
--- a/.releaserc.js
+++ b/.releaserc.js
@@ -1,5 +1,8 @@
module.exports = {
- branches: ["main"],
+ branches: [
+ {name: "main"},
+ {name: "next", prerelease: true},
+ ],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
diff --git a/NEXT_RELEASE.md b/NEXT_RELEASE.md
deleted file mode 100644
index 91f7f5d..0000000
--- a/NEXT_RELEASE.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Backwards-incompatible changes to be made in the next major release
-
-- Add `rp/RelyingParty.GetRevokeEndpoint`
-- Rename `op/OpStorage.GetKeyByIDAndUserID` to `op/OpStorage.GetKeyByIDAndClientID`
-- Add `CanRefreshTokenInfo` (`GetRefreshTokenInfo()`) to `op.Storage`
-
diff --git a/README.md b/README.md
index 1e4c26f..c47e192 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ The most important packages of the library:
/client/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
/client/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/client/service demonstration of JWT Profile Authorization Grant
- /server example of an OpenID Provider implementation including some very basic login UI
+ /server examples of an OpenID Provider implementations (including dynamic) with some very basic login UI
## How To Use It
@@ -44,16 +44,27 @@ Check the `/example` folder where example code for different scenarios is locate
```bash
# start oidc op server
# oidc discovery http://localhost:9998/.well-known/openid-configuration
-go run github.com/zitadel/oidc/example/server
+go run github.com/zitadel/oidc/v2/example/server
# start oidc web client (in a new terminal)
-CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998 SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/example/client/app
+CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://localhost:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
```
- open http://localhost:9999/login in your browser
- you will be redirected to op server and the login UI
-- login with user `test-user` and password `verysecure`
+- login with user `test-user@localhost` and password `verysecure`
- the OP will redirect you to the client app, which displays the user info
+for the dynamic issuer, just start it with:
+```bash
+go run github.com/zitadel/oidc/v2/example/server/dynamic
+```
+the oidc web client above will still work, but if you add `oidc.local` (pointing to 127.0.0.1) in your hosts file you can also start it with:
+```bash
+CLIENT_ID=web CLIENT_SECRET=secret ISSUER=http://oidc.local:9998/ SCOPES="openid profile" PORT=9999 go run github.com/zitadel/oidc/v2/example/client/app
+```
+
+> Note: Usernames are suffixed with the hostname (`test-user@localhost` or `test-user@oidc.local`)
+
## Features
| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials |
@@ -87,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 0ab669d..8093b63 100644
--- a/example/client/api/api.go
+++ b/example/client/api/api.go
@@ -12,8 +12,8 @@ import (
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
- "github.com/zitadel/oidc/pkg/client/rs"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client/rs"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
const (
@@ -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 e7be491..0c324d2 100644
--- a/example/client/app/app.go
+++ b/example/client/app/app.go
@@ -11,9 +11,9 @@ import (
"github.com/google/uuid"
"github.com/sirupsen/logrus"
- "github.com/zitadel/oidc/pkg/client/rp"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
var (
@@ -62,7 +62,7 @@ func main() {
http.Handle("/login", rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!")))
// 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)
@@ -82,6 +82,31 @@ func main() {
// w.Write(data)
//}
+ // you can also try token exchange flow
+ //
+ // requestTokenExchange := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
+ // data := make(url.Values)
+ // data.Set("grant_type", string(oidc.GrantTypeTokenExchange))
+ // data.Set("requested_token_type", string(oidc.IDTokenType))
+ // data.Set("subject_token", tokens.RefreshToken)
+ // data.Set("subject_token_type", string(oidc.RefreshTokenType))
+ // data.Add("scope", "profile custom_scope:impersonate:id2")
+
+ // client := &http.Client{}
+ // r2, _ := http.NewRequest(http.MethodPost, issuer+"/oauth/token", strings.NewReader(data.Encode()))
+ // // r2.Header.Add("Authorization", "Basic "+"d2ViOnNlY3JldA==")
+ // r2.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ // r2.SetBasicAuth("web", "secret")
+
+ // resp, _ := client.Do(r2)
+ // fmt.Println(resp.Status)
+
+ // b, _ := io.ReadAll(resp.Body)
+ // resp.Body.Close()
+
+ // w.Write(b)
+ // }
+
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
diff --git a/example/client/device/device.go b/example/client/device/device.go
new file mode 100644
index 0000000..284ba37
--- /dev/null
+++ b/example/client/device/device.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/sirupsen/logrus"
+
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+)
+
+var (
+ key = []byte("test1234test1234")
+)
+
+func main() {
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT)
+ defer stop()
+
+ clientID := os.Getenv("CLIENT_ID")
+ clientSecret := os.Getenv("CLIENT_SECRET")
+ keyPath := os.Getenv("KEY_PATH")
+ issuer := os.Getenv("ISSUER")
+ scopes := strings.Split(os.Getenv("SCOPES"), " ")
+
+ cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
+
+ var options []rp.Option
+ if clientSecret == "" {
+ options = append(options, rp.WithPKCE(cookieHandler))
+ }
+ if keyPath != "" {
+ options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
+ }
+
+ provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, "", scopes, options...)
+ if err != nil {
+ logrus.Fatalf("error creating provider %s", err.Error())
+ }
+
+ logrus.Info("starting device authorization flow")
+ resp, err := rp.DeviceAuthorization(scopes, provider)
+ if err != nil {
+ logrus.Fatal(err)
+ }
+ logrus.Info("resp", resp)
+ fmt.Printf("\nPlease browse to %s and enter code %s\n", resp.VerificationURI, resp.UserCode)
+
+ logrus.Info("start polling")
+ token, err := rp.DeviceAccessToken(ctx, resp.DeviceCode, time.Duration(resp.Interval)*time.Second, provider)
+ if err != nil {
+ logrus.Fatal(err)
+ }
+ logrus.Infof("successfully obtained token: %v", token)
+}
diff --git a/example/client/github/github.go b/example/client/github/github.go
index feb3e26..9cb813c 100644
--- a/example/client/github/github.go
+++ b/example/client/github/github.go
@@ -10,9 +10,10 @@ import (
"golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github"
- "github.com/zitadel/oidc/pkg/client/rp"
- "github.com/zitadel/oidc/pkg/client/rp/cli"
- "github.com/zitadel/oidc/pkg/http"
+ "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/client/service/service.go b/example/client/service/service.go
index b3819d5..9526174 100644
--- a/example/client/service/service.go
+++ b/example/client/service/service.go
@@ -13,7 +13,7 @@ import (
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
- "github.com/zitadel/oidc/pkg/client/profile"
+ "github.com/zitadel/oidc/v2/pkg/client/profile"
)
var client = http.DefaultClient
diff --git a/example/doc.go b/example/doc.go
index 7212a7d..fd4f038 100644
--- a/example/doc.go
+++ b/example/doc.go
@@ -5,7 +5,6 @@ Package example contains some example of the various use of this library:
/app web app / RP demonstrating authorization code flow using various authentication methods (code, PKCE, JWT profile)
/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/service demonstration of JWT Profile Authorization Grant
-/server example of an OpenID Provider implementation including some very basic login UI
-
+/server examples of an OpenID Provider implementations (including dynamic) with some very basic
*/
package example
diff --git a/example/server/dynamic/login.go b/example/server/dynamic/login.go
new file mode 100644
index 0000000..e7c6e5f
--- /dev/null
+++ b/example/server/dynamic/login.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ "github.com/zitadel/oidc/v2/pkg/op"
+)
+
+const (
+ queryAuthRequestID = "authRequestID"
+)
+
+var (
+ loginTmpl, _ = template.New("login").Parse(`
+
+
+
+
+ Login
+
+
+
+
+ `)
+)
+
+type login struct {
+ authenticate authenticate
+ router *mux.Router
+ callback func(context.Context, string) string
+}
+
+func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login {
+ l := &login{
+ authenticate: authenticate,
+ callback: callback,
+ }
+ l.createRouter(issuerInterceptor)
+ return l
+}
+
+func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
+ l.router = mux.NewRouter()
+ l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler)
+ l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler))
+}
+
+type authenticate interface {
+ CheckUsernamePassword(ctx context.Context, username, password, id string) error
+}
+
+func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
+ return
+ }
+ //the oidc package will pass the id of the auth request as query parameter
+ //we will use this id through the login process and therefore pass it to the login page
+ renderLogin(w, r.FormValue(queryAuthRequestID), nil)
+}
+
+func renderLogin(w http.ResponseWriter, id string, err error) {
+ var errMsg string
+ if err != nil {
+ errMsg = err.Error()
+ }
+ data := &struct {
+ ID string
+ Error string
+ }{
+ ID: id,
+ Error: errMsg,
+ }
+ err = loginTmpl.Execute(w, data)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError)
+ return
+ }
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ id := r.FormValue("id")
+ err = l.authenticate.CheckUsernamePassword(r.Context(), username, password, id)
+ if err != nil {
+ renderLogin(w, id, err)
+ return
+ }
+ http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
+}
diff --git a/example/server/dynamic/op.go b/example/server/dynamic/op.go
new file mode 100644
index 0000000..783c75c
--- /dev/null
+++ b/example/server/dynamic/op.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+ "context"
+ "crypto/sha256"
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "golang.org/x/text/language"
+
+ "github.com/zitadel/oidc/v2/example/server/storage"
+ "github.com/zitadel/oidc/v2/pkg/op"
+)
+
+const (
+ pathLoggedOut = "/logged-out"
+)
+
+var (
+ hostnames = []string{
+ "localhost", //note that calling 127.0.0.1 / ::1 won't work as the hostname does not match
+ "oidc.local", //add this to your hosts file (pointing to 127.0.0.1)
+ //feel free to add more...
+ }
+)
+
+func init() {
+ storage.RegisterClients(
+ storage.NativeClient("native"),
+ storage.WebClient("web", "secret"),
+ storage.WebClient("api", "secret"),
+ )
+}
+
+func main() {
+ ctx := context.Background()
+
+ port := "9998"
+ issuers := make([]string, len(hostnames))
+ for i, hostname := range hostnames {
+ issuers[i] = fmt.Sprintf("http://%s:%s/", hostname, port)
+ }
+
+ //the OpenID Provider requires a 32-byte key for (token) encryption
+ //be sure to create a proper crypto random key and manage it securely!
+ key := sha256.Sum256([]byte("test"))
+
+ router := mux.NewRouter()
+
+ //for simplicity, we provide a very small default page for users who have signed out
+ router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) {
+ _, err := w.Write([]byte("signed out successfully"))
+ if err != nil {
+ log.Printf("error serving logged out page: %v", err)
+ }
+ })
+
+ //the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
+ //this might be the layer for accessing your database
+ //in this example it will be handled in-memory
+ //the NewMultiStorage is able to handle multiple issuers
+ storage := storage.NewMultiStorage(issuers)
+
+ //creation of the OpenIDProvider with the just created in-memory Storage
+ provider, err := newDynamicOP(ctx, storage, key)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process
+ //for the simplicity of the example this means a simple page with username and password field
+ //be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage
+ l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
+
+ //regardless of how many pages / steps there are in the process, the UI must be registered in the router,
+ //so we will direct all calls to /login to the login UI
+ router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
+
+ //we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
+ //is served on the correct path
+ //
+ //if your issuer ends with a path (e.g. http://localhost:9998/custom/path/),
+ //then you would have to set the path prefix (/custom/path/):
+ //router.PathPrefix("/custom/path/").Handler(http.StripPrefix("/custom/path", provider.HttpHandler()))
+ router.PathPrefix("/").Handler(provider.HttpHandler())
+
+ server := &http.Server{
+ Addr: ":" + port,
+ Handler: router,
+ }
+ err = server.ListenAndServe()
+ if err != nil {
+ log.Fatal(err)
+ }
+ <-ctx.Done()
+}
+
+// newDynamicOP will create an OpenID Provider for localhost on a specified port with a given encryption key
+// and a predefined default logout uri
+// it will enable all options (see descriptions)
+func newDynamicOP(ctx context.Context, storage op.Storage, key [32]byte) (*op.Provider, error) {
+ config := &op.Config{
+ CryptoKey: key,
+
+ //will be used if the end_session endpoint is called without a post_logout_redirect_uri
+ DefaultLogoutRedirectURI: pathLoggedOut,
+
+ //enables code_challenge_method S256 for PKCE (and therefore PKCE in general)
+ CodeMethodS256: true,
+
+ //enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth)
+ AuthMethodPost: true,
+
+ //enables additional authentication by using private_key_jwt
+ AuthMethodPrivateKeyJWT: true,
+
+ //enables refresh_token grant use
+ GrantTypeRefreshToken: true,
+
+ //enables use of the `request` Object parameter
+ RequestObjectSupported: true,
+
+ //this example has only static texts (in English), so we'll set the here accordingly
+ SupportedUILocales: []language.Tag{language.English},
+ }
+ handler, err := op.NewDynamicOpenIDProvider("/", config, storage,
+ //we must explicitly allow the use of the http issuer
+ op.WithAllowInsecure(),
+ //as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
+ op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
+ )
+ if err != nil {
+ return nil, err
+ }
+ return handler, nil
+}
diff --git a/example/server/exampleop/device.go b/example/server/exampleop/device.go
new file mode 100644
index 0000000..ae2e8f2
--- /dev/null
+++ b/example/server/exampleop/device.go
@@ -0,0 +1,191 @@
+package exampleop
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+
+ "github.com/gorilla/mux"
+ "github.com/gorilla/securecookie"
+ "github.com/sirupsen/logrus"
+ "github.com/zitadel/oidc/v2/pkg/op"
+)
+
+type deviceAuthenticate interface {
+ CheckUsernamePasswordSimple(username, password string) error
+ op.DeviceAuthorizationStorage
+}
+
+type deviceLogin struct {
+ storage deviceAuthenticate
+ cookie *securecookie.SecureCookie
+}
+
+func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) {
+ l := &deviceLogin{
+ storage: storage,
+ cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil),
+ }
+
+ router.HandleFunc("", l.userCodeHandler)
+ router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler)
+ router.HandleFunc("/confirm", l.confirmHandler)
+}
+
+func renderUserCode(w io.Writer, err error) {
+ data := struct {
+ Error string
+ }{
+ Error: errMsg(err),
+ }
+
+ if err := templates.ExecuteTemplate(w, "usercode", data); err != nil {
+ logrus.Error(err)
+ }
+}
+
+func renderDeviceLogin(w http.ResponseWriter, userCode string, err error) {
+ data := &struct {
+ UserCode string
+ Error string
+ }{
+ UserCode: userCode,
+ Error: errMsg(err),
+ }
+ if err = templates.ExecuteTemplate(w, "device_login", data); err != nil {
+ logrus.Error(err)
+ }
+}
+
+func renderConfirmPage(w http.ResponseWriter, username, clientID string, scopes []string) {
+ data := &struct {
+ Username string
+ ClientID string
+ Scopes []string
+ }{
+ Username: username,
+ ClientID: clientID,
+ Scopes: scopes,
+ }
+ if err := templates.ExecuteTemplate(w, "confirm_device", data); err != nil {
+ logrus.Error(err)
+ }
+}
+
+func (d *deviceLogin) userCodeHandler(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ renderUserCode(w, err)
+ return
+ }
+ userCode := r.Form.Get("user_code")
+ if userCode == "" {
+ if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
+ err = errors.New(prompt)
+ }
+ renderUserCode(w, err)
+ return
+ }
+
+ renderDeviceLogin(w, userCode, nil)
+}
+
+func redirectBack(w http.ResponseWriter, r *http.Request, prompt string) {
+ values := make(url.Values)
+ values.Set("prompt", url.QueryEscape(prompt))
+
+ url := url.URL{
+ Path: "/device",
+ RawQuery: values.Encode(),
+ }
+ http.Redirect(w, r, url.String(), http.StatusSeeOther)
+}
+
+const userCodeCookieName = "user_code"
+
+type userCodeCookie struct {
+ UserCode string
+ UserName string
+}
+
+func (d *deviceLogin) loginHandler(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+
+ userCode := r.PostForm.Get("user_code")
+ if userCode == "" {
+ redirectBack(w, r, "missing user_code in request")
+ return
+ }
+ username := r.PostForm.Get("username")
+ if username == "" {
+ redirectBack(w, r, "missing username in request")
+ return
+ }
+ password := r.PostForm.Get("password")
+ if password == "" {
+ redirectBack(w, r, "missing password in request")
+ return
+ }
+
+ if err := d.storage.CheckUsernamePasswordSimple(username, password); err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+ state, err := d.storage.GetDeviceAuthorizationByUserCode(r.Context(), userCode)
+ if err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+
+ encoded, err := d.cookie.Encode(userCodeCookieName, userCodeCookie{userCode, username})
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ cookie := &http.Cookie{
+ Name: userCodeCookieName,
+ Value: encoded,
+ Path: "/",
+ }
+ http.SetCookie(w, cookie)
+ renderConfirmPage(w, username, state.ClientID, state.Scopes)
+}
+
+func (d *deviceLogin) confirmHandler(w http.ResponseWriter, r *http.Request) {
+ cookie, err := r.Cookie(userCodeCookieName)
+ if err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+ data := new(userCodeCookie)
+ if err = d.cookie.Decode(userCodeCookieName, cookie.Value, &data); err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+ if err = r.ParseForm(); err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+
+ action := r.Form.Get("action")
+ switch action {
+ case "allowed":
+ err = d.storage.CompleteDeviceAuthorization(r.Context(), data.UserCode, data.UserName)
+ case "denied":
+ err = d.storage.DenyDeviceAuthorization(r.Context(), data.UserCode)
+ default:
+ err = errors.New("action must be one of \"allow\" or \"deny\"")
+ }
+ if err != nil {
+ redirectBack(w, r, err.Error())
+ return
+ }
+
+ fmt.Fprintf(w, "Device authorization %s. You can now return to the device", action)
+}
diff --git a/example/server/exampleop/login.go b/example/server/exampleop/login.go
index fd3dead..c014c9a 100644
--- a/example/server/exampleop/login.go
+++ b/example/server/exampleop/login.go
@@ -1,53 +1,20 @@
package exampleop
import (
+ "context"
"fmt"
- "html/template"
"net/http"
"github.com/gorilla/mux"
)
-const (
- queryAuthRequestID = "authRequestID"
-)
-
-var loginTmpl, _ = template.New("login").Parse(`
-
-
-
-
- Login
-
-
-
-
- `)
-
type login struct {
authenticate authenticate
router *mux.Router
- callback func(string) string
+ callback func(context.Context, string) string
}
-func NewLogin(authenticate authenticate, callback func(string) string) *login {
+func NewLogin(authenticate authenticate, callback func(context.Context, string) string) *login {
l := &login{
authenticate: authenticate,
callback: callback,
@@ -73,23 +40,19 @@ func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
return
}
// the oidc package will pass the id of the auth request as query parameter
- // we will use this id through the login process and therefore pass it to the login page
+ // we will use this id through the login process and therefore pass it to the login page
renderLogin(w, r.FormValue(queryAuthRequestID), nil)
}
func renderLogin(w http.ResponseWriter, id string, err error) {
- var errMsg string
- if err != nil {
- errMsg = err.Error()
- }
data := &struct {
ID string
Error string
}{
ID: id,
- Error: errMsg,
+ Error: errMsg(err),
}
- err = loginTmpl.Execute(w, data)
+ err = templates.ExecuteTemplate(w, "login", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
@@ -109,5 +72,5 @@ func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
renderLogin(w, id, err)
return
}
- http.Redirect(w, r, l.callback(id), http.StatusFound)
+ http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
}
diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go
index 4794d8a..5604483 100644
--- a/example/server/exampleop/op.go
+++ b/example/server/exampleop/op.go
@@ -1,17 +1,16 @@
package exampleop
import (
- "context"
"crypto/sha256"
"log"
"net/http"
- "os"
+ "time"
"github.com/gorilla/mux"
"golang.org/x/text/language"
- "github.com/zitadel/oidc/example/server/storage"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/example/server/storage"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
const (
@@ -28,16 +27,14 @@ func init() {
type Storage interface {
op.Storage
- CheckUsernamePassword(username, password, id string) error
+ authenticate
+ deviceAuthenticate
}
// SetupServer creates an OIDC server with Issuer=http://localhost:
//
// Use one of the pre-made clients in storage/clients.go or register a new one.
-func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router {
- // this will allow us to use an issuer with http:// instead of https://
- os.Setenv(op.OidcDevMode, "true")
-
+func SetupServer(issuer string, storage Storage) *mux.Router {
// the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test"))
@@ -53,7 +50,7 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
})
// creation of the OpenIDProvider with the just created in-memory Storage
- provider, err := newOP(ctx, storage, issuer, key)
+ provider, err := newOP(storage, issuer, key)
if err != nil {
log.Fatal(err)
}
@@ -66,6 +63,9 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
// so we will direct all calls to /login to the login UI
router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router))
+ router.PathPrefix("/device").Subrouter()
+ registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter())
+
// we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration)
// is served on the correct path
//
@@ -79,9 +79,8 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key
// and a predefined default logout uri
// it will enable all options (see descriptions)
-func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
+func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
config := &op.Config{
- Issuer: issuer,
CryptoKey: key,
// will be used if the end_session endpoint is called without a post_logout_redirect_uri
@@ -104,8 +103,17 @@ func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte)
// this example has only static texts (in English), so we'll set the here accordingly
SupportedUILocales: []language.Tag{language.English},
+
+ DeviceAuthorization: op.DeviceAuthorizationConfig{
+ Lifetime: 5 * time.Minute,
+ PollInterval: 5 * time.Second,
+ UserFormURL: issuer + "device",
+ UserCode: op.UserCodeBase20,
+ },
}
- handler, err := op.NewOpenIDProvider(ctx, config, storage,
+ handler, err := op.NewOpenIDProvider(issuer, config, storage,
+ //we must explicitly allow the use of the http issuer
+ op.WithAllowInsecure(),
// as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
)
diff --git a/example/server/exampleop/templates.go b/example/server/exampleop/templates.go
new file mode 100644
index 0000000..5b5c966
--- /dev/null
+++ b/example/server/exampleop/templates.go
@@ -0,0 +1,26 @@
+package exampleop
+
+import (
+ "embed"
+ "html/template"
+
+ "github.com/sirupsen/logrus"
+)
+
+var (
+ //go:embed templates
+ templateFS embed.FS
+ templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
+)
+
+const (
+ queryAuthRequestID = "authRequestID"
+)
+
+func errMsg(err error) string {
+ if err == nil {
+ return ""
+ }
+ logrus.Error(err)
+ return err.Error()
+}
diff --git a/example/server/exampleop/templates/confirm_device.html b/example/server/exampleop/templates/confirm_device.html
new file mode 100644
index 0000000..a6bcdad
--- /dev/null
+++ b/example/server/exampleop/templates/confirm_device.html
@@ -0,0 +1,25 @@
+{{ define "confirm_device" -}}
+
+
+
+
+ Confirm device authorization
+
+
+
+ Welcome back {{.Username}}!
+
+ You are about to grant device {{.ClientID}} access to the following scopes: {{.Scopes}}.
+
+
+
+
+
+{{- end }}
diff --git a/example/server/exampleop/templates/device_login.html b/example/server/exampleop/templates/device_login.html
new file mode 100644
index 0000000..cc5b00b
--- /dev/null
+++ b/example/server/exampleop/templates/device_login.html
@@ -0,0 +1,29 @@
+{{ define "device_login" -}}
+
+
+
+
+ Login
+
+
+
+
+
+{{- end }}
diff --git a/example/server/exampleop/templates/login.html b/example/server/exampleop/templates/login.html
new file mode 100644
index 0000000..b048211
--- /dev/null
+++ b/example/server/exampleop/templates/login.html
@@ -0,0 +1,29 @@
+{{ define "login" -}}
+
+
+
+
+ Login
+
+
+
+
+`
+{{- end }}
\ No newline at end of file
diff --git a/example/server/exampleop/templates/usercode.html b/example/server/exampleop/templates/usercode.html
new file mode 100644
index 0000000..fb8fa7f
--- /dev/null
+++ b/example/server/exampleop/templates/usercode.html
@@ -0,0 +1,21 @@
+{{ define "usercode" -}}
+
+
+
+
+ Device authorization
+
+
+
+
+
+{{- end }}
diff --git a/example/server/main.go b/example/server/main.go
index 3cfd20d..a2836ea 100644
--- a/example/server/main.go
+++ b/example/server/main.go
@@ -1,24 +1,26 @@
package main
import (
- "context"
+ "fmt"
"log"
"net/http"
- "github.com/zitadel/oidc/example/server/exampleop"
- "github.com/zitadel/oidc/example/server/storage"
+ "github.com/zitadel/oidc/v2/example/server/exampleop"
+ "github.com/zitadel/oidc/v2/example/server/storage"
)
func main() {
- ctx := context.Background()
+ //we will run on :9998
+ port := "9998"
+ //which gives us the issuer: http://localhost:9998/
+ issuer := fmt.Sprintf("http://localhost:%s/", port)
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database
// in this example it will be handled in-memory
- storage := storage.NewStorage(storage.NewUserStore())
+ storage := storage.NewStorage(storage.NewUserStore(issuer))
- port := "9998"
- router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage)
+ router := exampleop.SetupServer(issuer, storage)
server := &http.Server{
Addr: ":" + port,
@@ -30,5 +32,4 @@ func main() {
if err != nil {
log.Fatal(err)
}
- <-ctx.Done()
}
diff --git a/example/server/storage/client.go b/example/server/storage/client.go
index 0b98679..b850053 100644
--- a/example/server/storage/client.go
+++ b/example/server/storage/client.go
@@ -3,8 +3,8 @@ package storage
import (
"time"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
var (
@@ -168,7 +168,7 @@ func NativeClient(id string, redirectURIs ...string) *Client {
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
- accessTokenType: 0,
+ accessTokenType: op.AccessTokenTypeBearer,
devMode: false,
idTokenUserinfoClaimsAssertion: false,
clockSkew: 0,
@@ -194,7 +194,7 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
- accessTokenType: 0,
+ accessTokenType: op.AccessTokenTypeBearer,
devMode: false,
idTokenUserinfoClaimsAssertion: false,
clockSkew: 0,
diff --git a/example/server/storage/oidc.go b/example/server/storage/oidc.go
index 91afd90..f5412cf 100644
--- a/example/server/storage/oidc.go
+++ b/example/server/storage/oidc.go
@@ -5,9 +5,8 @@ import (
"golang.org/x/text/language"
- "github.com/zitadel/oidc/pkg/op"
-
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
const (
@@ -17,6 +16,9 @@ const (
// CustomClaim is an example for how to return custom claims with this library
CustomClaim = "custom_claim"
+
+ // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage
+ CustomScopeImpersonatePrefix = "custom_scope:impersonate:"
)
type AuthRequest struct {
@@ -35,8 +37,8 @@ type AuthRequest struct {
Nonce string
CodeChallenge *OIDCCodeChallenge
- passwordChecked bool
- authTime time.Time
+ done bool
+ authTime time.Time
}
func (a *AuthRequest) GetID() string {
@@ -49,7 +51,7 @@ func (a *AuthRequest) GetACR() string {
func (a *AuthRequest) GetAMR() []string {
// this example only uses password for authentication
- if a.passwordChecked {
+ if a.done {
return []string{"pwd"}
}
return nil
@@ -100,7 +102,7 @@ func (a *AuthRequest) GetSubject() string {
}
func (a *AuthRequest) Done() bool {
- return a.passwordChecked // this example only uses password for authentication
+ return a.done
}
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go
index 130822e..7e1afbd 100644
--- a/example/server/storage/storage.go
+++ b/example/server/storage/storage.go
@@ -4,16 +4,18 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
+ "errors"
"fmt"
"math/big"
+ "strings"
"sync"
"time"
"github.com/google/uuid"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
@@ -26,8 +28,8 @@ var serviceKey1 = &rsa.PublicKey{
E: 65537,
}
-// var _ op.Storage = &storage{}
-// var _ op.ClientCredentialsStorage = &storage{}
+var _ op.Storage = &Storage{}
+var _ op.ClientCredentialsStorage = &Storage{}
// storage implements the op.Storage interface
// typically you would implement this as a layer on top of your database
@@ -42,12 +44,47 @@ type Storage struct {
services map[string]Service
refreshTokens map[string]*RefreshToken
signingKey signingKey
+ deviceCodes map[string]deviceAuthorizationEntry
+ userCodes map[string]string
+ serviceUsers map[string]*Client
}
type signingKey struct {
- ID string
- Algorithm string
- Key *rsa.PrivateKey
+ id string
+ algorithm jose.SignatureAlgorithm
+ key *rsa.PrivateKey
+}
+
+func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
+ return s.algorithm
+}
+
+func (s *signingKey) Key() interface{} {
+ return s.key
+}
+
+func (s *signingKey) ID() string {
+ return s.id
+}
+
+type publicKey struct {
+ signingKey
+}
+
+func (s *publicKey) ID() string {
+ return s.id
+}
+
+func (s *publicKey) Algorithm() jose.SignatureAlgorithm {
+ return s.algorithm
+}
+
+func (s *publicKey) Use() string {
+ return "sig"
+}
+
+func (s *publicKey) Key() interface{} {
+ return &s.key.PublicKey
}
func NewStorage(userStore UserStore) *Storage {
@@ -67,9 +104,21 @@ func NewStorage(userStore UserStore) *Storage {
},
},
signingKey: signingKey{
- ID: "id",
- Algorithm: "RS256",
- Key: key,
+ id: uuid.NewString(),
+ algorithm: jose.RS256,
+ key: key,
+ },
+ deviceCodes: make(map[string]deviceAuthorizationEntry),
+ userCodes: make(map[string]string),
+ serviceUsers: map[string]*Client{
+ "sid1": {
+ id: "sid1",
+ secret: "verysecret",
+ grantTypes: []oidc.GrantType{
+ oidc.GrantTypeClientCredentials,
+ },
+ accessTokenType: op.AccessTokenTypeBearer,
+ },
},
}
}
@@ -95,7 +144,18 @@ func (s *Storage) CheckUsernamePassword(username, password, id string) error {
// you will have to change some state on the request to guide the user through possible multiple steps of the login process
// in this example we'll simply check the username / password and set a boolean to true
// therefore we will also just check this boolean if the request / login has been finished
- request.passwordChecked = true
+ request.done = true
+ return nil
+ }
+ return fmt.Errorf("username or password wrong")
+}
+
+func (s *Storage) CheckUsernamePasswordSimple(username, password string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ user := s.userStore.GetUserByUsername(username)
+ if user != nil && user.Password == password {
return nil
}
return fmt.Errorf("username or password wrong")
@@ -181,11 +241,14 @@ func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error {
// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
var applicationID string
- // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
- authReq, ok := request.(*AuthRequest)
- if ok {
- applicationID = authReq.ApplicationID
+ switch req := request.(type) {
+ case *AuthRequest:
+ // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
+ applicationID = req.ApplicationID
+ case op.TokenExchangeRequest:
+ applicationID = req.GetClientID()
}
+
token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes())
if err != nil {
return "", time.Time{}, err
@@ -196,6 +259,11 @@ func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest
// CreateAccessAndRefreshTokens implements the op.Storage interface
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
+ // generate tokens via token exchange flow if request is relevant
+ if teReq, ok := request.(op.TokenExchangeRequest); ok {
+ return s.exchangeRefreshToken(ctx, teReq)
+ }
+
// get the information depending on the request type / implementation
applicationID, authTime, amr := getInfoFromRequest(request)
@@ -226,6 +294,24 @@ func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.T
return accessToken.ID, refreshToken, accessToken.Expiration, nil
}
+func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
+ applicationID := request.GetClientID()
+ authTime := request.GetAuthTime()
+
+ refreshTokenID := uuid.NewString()
+ accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+
+ refreshToken, err := s.createRefreshToken(accessToken, nil, authTime)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+
+ return accessToken.ID, refreshToken, accessToken.Expiration, nil
+}
+
// TokenRequestByRefreshToken implements the op.Storage interface
// it will be called after parsing and validation of the refresh token request
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
@@ -252,6 +338,16 @@ func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID
return nil
}
+// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
+// If given something that is not a refresh token, it must return error.
+func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
+ refreshToken, ok := s.refreshTokens[token]
+ if !ok {
+ return "", "", op.ErrInvalidRefreshToken
+ }
+ return refreshToken.UserID, refreshToken.ID, nil
+}
+
// RevokeToken implements the op.Storage interface
// it will be called after parsing and validation of the token revocation request
func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error {
@@ -288,41 +384,29 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
return nil
}
-// GetSigningKey implements the op.Storage interface
+// SigningKey implements the op.Storage interface
// it will be called when creating the OpenID Provider
-func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) {
+func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) {
// in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256
// you would obviously have a more complex implementation and store / retrieve the key from your database as well
- //
- // the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and
- // switch the key of the signer via this channel
- keyCh <- jose.SigningKey{
- Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), // always tell the signer with algorithm to use
- Key: jose.JSONWebKey{
- KeyID: s.signingKey.ID, // always give the key an id so, that it will include it in the token header as `kid` claim
- Key: s.signingKey.Key,
- },
- }
+ return &s.signingKey, nil
}
-// GetKeySet implements the op.Storage interface
+// SignatureAlgorithms implements the op.Storage interface
+// it will be called to get the sign
+func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) {
+ return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil
+}
+
+// KeySet implements the op.Storage interface
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
-func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) {
+func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
// as mentioned above, this example only has a single signing key without key rotation,
// so it will directly use its public key
//
// when using key rotation you typically would store the public keys alongside the private keys in your database
- // and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every
- return &jose.JSONWebKeySet{
- Keys: []jose.JSONWebKey{
- {
- KeyID: s.signingKey.ID,
- Algorithm: s.signingKey.Algorithm,
- Use: oidc.KeyUseSignature,
- Key: &s.signingKey.Key.PublicKey,
- },
- },
- }, nil
+ // and give both of them an expiration date, with the public key having a longer lifetime
+ return []op.Key{&publicKey{s.signingKey}}, nil
}
// GetClientByClientID implements the op.Storage interface
@@ -356,13 +440,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()
@@ -390,7 +474,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()
@@ -407,14 +491,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
}
}
@@ -424,6 +511,10 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o
// GetPrivateClaimsFromScopes implements the op.Storage interface
// it will be called for the creation of a JWT access token to assert claims for custom scopes
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
+ return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
+}
+
+func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
for _, scope := range scopes {
switch scope {
case CustomScope:
@@ -433,9 +524,9 @@ func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, client
return claims, nil
}
-// GetKeyByIDAndUserID implements the op.Storage interface
+// GetKeyByIDAndClientID implements the op.Storage interface
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
-func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
+func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
s.lock.Lock()
defer s.lock.Unlock()
service, ok := s.services[clientID]
@@ -531,7 +622,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)
@@ -541,17 +632,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))
@@ -560,6 +653,101 @@ func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter,
return nil
}
+// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
+// it will be called to validate parsed Token Exchange Grant request
+func (s *Storage) ValidateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
+ if request.GetRequestedTokenType() == "" {
+ request.SetRequestedTokenType(oidc.RefreshTokenType)
+ }
+
+ // Just an example, some use cases might need this use case
+ if request.GetExchangeSubjectTokenType() == oidc.IDTokenType && request.GetRequestedTokenType() == oidc.RefreshTokenType {
+ return errors.New("exchanging id_token to refresh_token is not supported")
+ }
+
+ // Check impersonation permissions
+ if request.GetExchangeActor() == "" && !s.userStore.GetUserByID(request.GetExchangeSubject()).IsAdmin {
+ return errors.New("user doesn't have impersonation permission")
+ }
+
+ allowedScopes := make([]string, 0)
+ for _, scope := range request.GetScopes() {
+ if scope == oidc.ScopeAddress {
+ continue
+ }
+
+ if strings.HasPrefix(scope, CustomScopeImpersonatePrefix) {
+ subject := strings.TrimPrefix(scope, CustomScopeImpersonatePrefix)
+ request.SetSubject(subject)
+ }
+
+ allowedScopes = append(allowedScopes, scope)
+ }
+
+ request.SetCurrentScopes(allowedScopes)
+
+ return nil
+}
+
+// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface
+// Common use case is to store request for audit purposes. For this example we skip the storing.
+func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error {
+ return nil
+}
+
+// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface
+// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes
+// plus adding token exchange specific claims related to delegation or impersonation
+func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) {
+ claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes())
+ if err != nil {
+ return nil, err
+ }
+
+ for k, v := range s.getTokenExchangeClaims(ctx, request) {
+ claims = appendClaim(claims, k, v)
+ }
+
+ return claims, nil
+}
+
+// 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.UserInfo, request op.TokenExchangeRequest) error {
+ err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes())
+ if err != nil {
+ return err
+ }
+
+ for k, v := range s.getTokenExchangeClaims(ctx, request) {
+ userinfo.AppendClaims(k, v)
+ }
+
+ return nil
+}
+
+func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) {
+ for _, scope := range request.GetScopes() {
+ switch {
+ case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "":
+ // Set actor subject claim for impersonation flow
+ claims = appendClaim(claims, "act", map[string]interface{}{
+ "sub": request.GetExchangeSubject(),
+ })
+ }
+ }
+
+ // Set actor subject claim for delegation flow
+ // if request.GetExchangeActor() != "" {
+ // claims = appendClaim(claims, "act", map[string]interface{}{
+ // "sub": request.GetExchangeActor(),
+ // })
+ // }
+
+ return claims
+}
+
// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation
func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) {
authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access)
@@ -588,3 +776,126 @@ func appendClaim(claims map[string]interface{}, claim string, value interface{})
claims[claim] = value
return claims
}
+
+type deviceAuthorizationEntry struct {
+ deviceCode string
+ userCode string
+ state *op.DeviceAuthorizationState
+}
+
+func (s *Storage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if _, ok := s.clients[clientID]; !ok {
+ return errors.New("client not found")
+ }
+
+ if _, ok := s.userCodes[userCode]; ok {
+ return op.ErrDuplicateUserCode
+ }
+
+ s.deviceCodes[deviceCode] = deviceAuthorizationEntry{
+ deviceCode: deviceCode,
+ userCode: userCode,
+ state: &op.DeviceAuthorizationState{
+ ClientID: clientID,
+ Scopes: scopes,
+ Expires: expires,
+ },
+ }
+
+ s.userCodes[userCode] = deviceCode
+ return nil
+}
+
+func (s *Storage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) {
+ if ctx.Err() != nil {
+ return nil, ctx.Err()
+ }
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ entry, ok := s.deviceCodes[deviceCode]
+ if !ok || entry.state.ClientID != clientID {
+ return nil, errors.New("device code not found for client") // is there a standard not found error in the framework?
+ }
+
+ return entry.state, nil
+}
+
+func (s *Storage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ entry, ok := s.deviceCodes[s.userCodes[userCode]]
+ if !ok {
+ return nil, errors.New("user code not found")
+ }
+
+ return entry.state, nil
+}
+
+func (s *Storage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ entry, ok := s.deviceCodes[s.userCodes[userCode]]
+ if !ok {
+ return errors.New("user code not found")
+ }
+
+ entry.state.Subject = subject
+ entry.state.Done = true
+ return nil
+}
+
+func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.deviceCodes[s.userCodes[userCode]].state.Denied = true
+ return nil
+}
+
+// AuthRequestDone is used by testing and is not required to implement op.Storage
+func (s *Storage) AuthRequestDone(id string) error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if req, ok := s.authRequests[id]; ok {
+ req.done = true
+ return nil
+ }
+
+ return errors.New("request not found")
+}
+
+func (s *Storage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ client, ok := s.serviceUsers[clientID]
+ if !ok {
+ return nil, errors.New("wrong service user or password")
+ }
+ if client.secret != clientSecret {
+ return nil, errors.New("wrong service user or password")
+ }
+
+ return client, nil
+}
+
+func (s *Storage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (op.TokenRequest, error) {
+ client, ok := s.serviceUsers[clientID]
+ if !ok {
+ return nil, errors.New("wrong service user or password")
+ }
+
+ return &oidc.JWTTokenRequest{
+ Subject: client.id,
+ Audience: []string{clientID},
+ Scopes: scopes,
+ }, nil
+}
diff --git a/example/server/storage/storage_dynamic.go b/example/server/storage/storage_dynamic.go
new file mode 100644
index 0000000..6e5ee32
--- /dev/null
+++ b/example/server/storage/storage_dynamic.go
@@ -0,0 +1,270 @@
+package storage
+
+import (
+ "context"
+ "time"
+
+ "gopkg.in/square/go-jose.v2"
+
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+)
+
+type multiStorage struct {
+ issuers map[string]*Storage
+}
+
+// NewMultiStorage implements the op.Storage interface by wrapping multiple storage structs
+// and selecting them by the calling issuer
+func NewMultiStorage(issuers []string) *multiStorage {
+ s := make(map[string]*Storage)
+ for _, issuer := range issuers {
+ s[issuer] = NewStorage(NewUserStore(issuer))
+ }
+ return &multiStorage{issuers: s}
+}
+
+// CheckUsernamePassword implements the `authenticate` interface of the login
+func (s *multiStorage) CheckUsernamePassword(ctx context.Context, username, password, id string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.CheckUsernamePassword(username, password, id)
+}
+
+// CreateAuthRequest implements the op.Storage interface
+// it will be called after parsing and validation of the authentication request
+func (s *multiStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.CreateAuthRequest(ctx, authReq, userID)
+}
+
+// AuthRequestByID implements the op.Storage interface
+// it will be called after the Login UI redirects back to the OIDC endpoint
+func (s *multiStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.AuthRequestByID(ctx, id)
+}
+
+// AuthRequestByCode implements the op.Storage interface
+// it will be called after parsing and validation of the token request (in an authorization code flow)
+func (s *multiStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.AuthRequestByCode(ctx, code)
+}
+
+// SaveAuthCode implements the op.Storage interface
+// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri
+// (in an authorization code flow)
+func (s *multiStorage) SaveAuthCode(ctx context.Context, id string, code string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.SaveAuthCode(ctx, id, code)
+}
+
+// DeleteAuthRequest implements the op.Storage interface
+// it will be called after creating the token response (id and access tokens) for a valid
+// - authentication request (in an implicit flow)
+// - token request (in an authorization code flow)
+func (s *multiStorage) DeleteAuthRequest(ctx context.Context, id string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.DeleteAuthRequest(ctx, id)
+}
+
+// CreateAccessToken implements the op.Storage interface
+// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...)
+func (s *multiStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return "", time.Time{}, err
+ }
+ return storage.CreateAccessToken(ctx, request)
+}
+
+// CreateAccessAndRefreshTokens implements the op.Storage interface
+// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
+func (s *multiStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return "", "", time.Time{}, err
+ }
+ return storage.CreateAccessAndRefreshTokens(ctx, request, currentRefreshToken)
+}
+
+// TokenRequestByRefreshToken implements the op.Storage interface
+// it will be called after parsing and validation of the refresh token request
+func (s *multiStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.TokenRequestByRefreshToken(ctx, refreshToken)
+}
+
+// TerminateSession implements the op.Storage interface
+// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed
+func (s *multiStorage) TerminateSession(ctx context.Context, userID string, clientID string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.TerminateSession(ctx, userID, clientID)
+}
+
+// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id.
+// If given something that is not a refresh token, it must return error.
+func (s *multiStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return "", "", err
+ }
+ return storage.GetRefreshTokenInfo(ctx, clientID, token)
+}
+
+// RevokeToken implements the op.Storage interface
+// it will be called after parsing and validation of the token revocation request
+func (s *multiStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.RevokeToken(ctx, token, userID, clientID)
+}
+
+// SigningKey implements the op.Storage interface
+// it will be called when creating the OpenID Provider
+func (s *multiStorage) SigningKey(ctx context.Context) (op.SigningKey, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.SigningKey(ctx)
+}
+
+// SignatureAlgorithms implements the op.Storage interface
+// it will be called to get the sign
+func (s *multiStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.SignatureAlgorithms(ctx)
+}
+
+// KeySet implements the op.Storage interface
+// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
+func (s *multiStorage) KeySet(ctx context.Context) ([]op.Key, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.KeySet(ctx)
+}
+
+// GetClientByClientID implements the op.Storage interface
+// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed
+func (s *multiStorage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.GetClientByClientID(ctx, clientID)
+}
+
+// AuthorizeClientIDSecret implements the op.Storage interface
+// it will be called for validating the client_id, client_secret on token or introspection requests
+func (s *multiStorage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.AuthorizeClientIDSecret(ctx, clientID, clientSecret)
+}
+
+// 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.UserInfo, userID, clientID string, scopes []string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.SetUserinfoFromScopes(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 *multiStorage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.SetUserinfoFromToken(ctx, userinfo, tokenID, subject, origin)
+}
+
+// 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 {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return err
+ }
+ return storage.SetIntrospectionFromToken(ctx, introspection, tokenID, subject, clientID)
+}
+
+// GetPrivateClaimsFromScopes implements the op.Storage interface
+// it will be called for the creation of a JWT access token to assert claims for custom scopes
+func (s *multiStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.GetPrivateClaimsFromScopes(ctx, userID, clientID, scopes)
+}
+
+// GetKeyByIDAndClientID implements the op.Storage interface
+// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
+func (s *multiStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.GetKeyByIDAndClientID(ctx, keyID, userID)
+}
+
+// ValidateJWTProfileScopes implements the op.Storage interface
+// it will be called to validate the scopes of a JWT Profile Authorization Grant request
+func (s *multiStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
+ storage, err := s.storageFromContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return storage.ValidateJWTProfileScopes(ctx, userID, scopes)
+}
+
+// Health implements the op.Storage interface
+func (s *multiStorage) Health(ctx context.Context) error {
+ return nil
+}
+
+func (s *multiStorage) storageFromContext(ctx context.Context) (*Storage, *oidc.Error) {
+ storage, ok := s.issuers[op.IssuerFromContext(ctx)]
+ if !ok {
+ return nil, oidc.ErrInvalidRequest().WithDescription("invalid issuer")
+ }
+ return storage, nil
+}
diff --git a/example/server/storage/user.go b/example/server/storage/user.go
index 423af59..173daef 100644
--- a/example/server/storage/user.go
+++ b/example/server/storage/user.go
@@ -2,6 +2,7 @@ package storage
import (
"crypto/rsa"
+ "strings"
"golang.org/x/text/language"
)
@@ -17,6 +18,7 @@ type User struct {
Phone string
PhoneVerified bool
PreferredLanguage language.Tag
+ IsAdmin bool
}
type Service struct {
@@ -33,12 +35,13 @@ type userStore struct {
users map[string]*User
}
-func NewUserStore() UserStore {
+func NewUserStore(issuer string) UserStore {
+ hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
return userStore{
users: map[string]*User{
"id1": {
ID: "id1",
- Username: "test-user",
+ Username: "test-user@" + hostname,
Password: "verysecure",
FirstName: "Test",
LastName: "User",
@@ -47,6 +50,20 @@ func NewUserStore() UserStore {
Phone: "",
PhoneVerified: false,
PreferredLanguage: language.German,
+ IsAdmin: true,
+ },
+ "id2": {
+ ID: "id2",
+ Username: "test-user2",
+ Password: "verysecure",
+ FirstName: "Test",
+ LastName: "User2",
+ Email: "test-user2@zitadel.ch",
+ EmailVerified: true,
+ Phone: "",
+ PhoneVerified: false,
+ PreferredLanguage: language.German,
+ IsAdmin: false,
},
},
}
diff --git a/go.mod b/go.mod
index d8b4465..7594264 100644
--- a/go.mod
+++ b/go.mod
@@ -1,23 +1,35 @@
-module github.com/zitadel/oidc
+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.1
github.com/rs/cors v1.8.3
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.2
- github.com/zitadel/logging v0.3.4
- golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
- golang.org/x/text v0.7.0
- gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
+ golang.org/x/oauth2 v0.6.0
+ golang.org/x/text v0.8.0
gopkg.in/square/go-jose.v2 v2.6.0
)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-cmp v0.5.9 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/crypto v0.7.0 // indirect
+ golang.org/x/net v0.8.0 // indirect
+ golang.org/x/sys v0.6.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/protobuf v1.29.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
index 6ee11f2..e4e5c6c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,144 +1,50 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
-github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
+github.com/muhlemmer/gu v0.3.1/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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
-github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -148,140 +54,34 @@ 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=
-github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM=
-github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0=
-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=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-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/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
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=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-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=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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-20220207234003-57398862261d/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/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/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=
@@ -290,149 +90,27 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.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=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
-gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
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 62f1019..9eda973 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -1,31 +1,25 @@
package client
import (
+ "context"
+ "encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
- "reflect"
"strings"
"time"
- "github.com/gorilla/schema"
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/crypto"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/crypto"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
-var Encoder = func() httphelper.Encoder {
- e := schema.NewEncoder()
- e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string {
- return value.Interface().(oidc.SpaceDelimitedArray).Encode()
- })
- return e
-}()
+var Encoder = httphelper.Encoder(oidc.NewEncoder())
// Discover calls the discovery endpoint of the provided issuer and returns its configuration
// It accepts an optional argument "wellknownUrl" which can be used to overide the dicovery endpoint url
@@ -90,6 +84,9 @@ func CallEndSessionEndpoint(request interface{}, authFn interface{}, caller EndS
return http.ErrUseLastResponse
}
resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := io.ReadAll(resp.Body)
@@ -148,6 +145,18 @@ func CallRevokeEndpoint(request interface{}, authFn interface{}, caller RevokeCa
return nil
}
+func CallTokenExchangeEndpoint(request interface{}, authFn interface{}, caller TokenEndpointCaller) (resp *oidc.TokenExchangeResponse, err error) {
+ req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn)
+ if err != nil {
+ return nil, err
+ }
+ tokenRes := new(oidc.TokenExchangeResponse)
+ if err := httphelper.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil {
+ return nil, err
+ }
+ return tokenRes, nil
+}
+
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
privateKey, err := crypto.BytesToPrivateKey(key)
if err != nil {
@@ -167,7 +176,98 @@ 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)
}
+
+type DeviceAuthorizationCaller interface {
+ GetDeviceAuthorizationEndpoint() string
+ HttpClient() *http.Client
+}
+
+func CallDeviceAuthorizationEndpoint(request *oidc.ClientCredentialsRequest, caller DeviceAuthorizationCaller) (*oidc.DeviceAuthorizationResponse, error) {
+ req, err := httphelper.FormRequest(caller.GetDeviceAuthorizationEndpoint(), request, Encoder, nil)
+ if err != nil {
+ return nil, err
+ }
+ if request.ClientSecret != "" {
+ req.SetBasicAuth(request.ClientID, request.ClientSecret)
+ }
+
+ resp := new(oidc.DeviceAuthorizationResponse)
+ if err := httphelper.HttpRequest(caller.HttpClient(), req, &resp); err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+type DeviceAccessTokenRequest struct {
+ *oidc.ClientCredentialsRequest
+ oidc.DeviceAccessTokenRequest
+}
+
+func CallDeviceAccessTokenEndpoint(ctx context.Context, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
+ req, err := httphelper.FormRequest(caller.TokenEndpoint(), request, Encoder, nil)
+ if err != nil {
+ return nil, err
+ }
+ if request.ClientSecret != "" {
+ req.SetBasicAuth(request.ClientID, request.ClientSecret)
+ }
+
+ httpResp, err := caller.HttpClient().Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer httpResp.Body.Close()
+
+ resp := new(struct {
+ *oidc.AccessTokenResponse
+ *oidc.Error
+ })
+ if err = json.NewDecoder(httpResp.Body).Decode(resp); err != nil {
+ return nil, err
+ }
+
+ if httpResp.StatusCode == http.StatusOK {
+ return resp.AccessTokenResponse, nil
+ }
+
+ return nil, resp.Error
+}
+
+func PollDeviceAccessTokenEndpoint(ctx context.Context, interval time.Duration, request *DeviceAccessTokenRequest, caller TokenEndpointCaller) (*oidc.AccessTokenResponse, error) {
+ for {
+ timer := time.After(interval)
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-timer:
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, interval)
+ defer cancel()
+
+ resp, err := CallDeviceAccessTokenEndpoint(ctx, request, caller)
+ if err == nil {
+ return resp, nil
+ }
+ if errors.Is(err, context.DeadlineExceeded) {
+ interval += 5 * time.Second
+ }
+ var target *oidc.Error
+ if !errors.As(err, &target) {
+ return nil, err
+ }
+ switch target.ErrorType {
+ case oidc.AuthorizationPending:
+ continue
+ case oidc.SlowDown:
+ interval += 5 * time.Second
+ continue
+ default:
+ return nil, err
+ }
+ }
+}
diff --git a/pkg/client/rp/integration_test.go b/pkg/client/integration_test.go
similarity index 69%
rename from pkg/client/rp/integration_test.go
rename to pkg/client/integration_test.go
index 732a4bf..e19a720 100644
--- a/pkg/client/rp/integration_test.go
+++ b/pkg/client/integration_test.go
@@ -1,8 +1,7 @@
-package rp_test
+package client_test
import (
"bytes"
- "context"
"io"
"io/ioutil"
"math/rand"
@@ -15,34 +14,142 @@ import (
"testing"
"time"
- "github.com/zitadel/oidc/example/server/exampleop"
- "github.com/zitadel/oidc/example/server/storage"
-
"github.com/jeremija/gosubmit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/zitadel/oidc/pkg/client/rp"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+
+ "github.com/zitadel/oidc/v2/example/server/exampleop"
+ "github.com/zitadel/oidc/v2/example/server/storage"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ "github.com/zitadel/oidc/v2/pkg/client/rs"
+ "github.com/zitadel/oidc/v2/pkg/client/tokenexchange"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
func TestRelyingPartySession(t *testing.T) {
t.Log("------- start example OP ------")
- ctx := context.Background()
- exampleStorage := storage.NewStorage(storage.NewUserStore())
+ targetURL := "http://local-site"
+ exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler
opServer := httptest.NewServer(&dh)
defer opServer.Close()
t.Logf("auth server at %s", opServer.URL)
- dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage)
+ dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
+ seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
+ clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
+
+ t.Log("------- run authorization code flow ------")
+ provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, "secret")
+
+ t.Log("------- refresh tokens ------")
+
+ newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
+ require.NoError(t, err, "refresh token")
+ assert.NotNil(t, newTokens, "access token")
+ t.Logf("new access token %s", newTokens.AccessToken)
+ t.Logf("new refresh token %s", newTokens.RefreshToken)
+ t.Logf("new token type %s", newTokens.TokenType)
+ t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
+ require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
+
+ t.Log("------ end session (logout) ------")
+
+ newLoc, err := rp.EndSession(provider, idToken, "", "")
+ require.NoError(t, err, "logout")
+ if newLoc != nil {
+ t.Logf("redirect to %s", newLoc)
+ } else {
+ t.Logf("no redirect")
+ }
+
+ t.Log("------ attempt refresh again (should fail) ------")
+ t.Log("trying original refresh token", refreshToken)
+ _, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
+ assert.Errorf(t, err, "refresh with original")
+ if newTokens.RefreshToken != "" {
+ t.Log("trying replacement refresh token", newTokens.RefreshToken)
+ _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
+ assert.Errorf(t, err, "refresh with replacement")
+ }
+}
+
+func TestResourceServerTokenExchange(t *testing.T) {
+ t.Log("------- start example OP ------")
+ targetURL := "http://local-site"
+ exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
+ var dh deferredHandler
+ opServer := httptest.NewServer(&dh)
+ defer opServer.Close()
+ t.Logf("auth server at %s", opServer.URL)
+ dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage)
+
+ seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
+ clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
+ clientSecret := "secret"
+
+ t.Log("------- run authorization code flow ------")
+ provider, _, refreshToken, idToken := RunAuthorizationCodeFlow(t, opServer, clientID, clientSecret)
+
+ resourceServer, err := rs.NewResourceServerClientCredentials(opServer.URL, clientID, clientSecret)
+ require.NoError(t, err, "new resource server")
+
+ t.Log("------- exchage refresh tokens (impersonation) ------")
+
+ tokenExchangeResponse, err := tokenexchange.ExchangeToken(
+ resourceServer,
+ refreshToken,
+ oidc.RefreshTokenType,
+ "",
+ "",
+ []string{},
+ []string{},
+ []string{"profile", "custom_scope:impersonate:id2"},
+ oidc.RefreshTokenType,
+ )
+ require.NoError(t, err, "refresh token")
+ require.NotNil(t, tokenExchangeResponse, "token exchange response")
+ assert.Equal(t, tokenExchangeResponse.IssuedTokenType, oidc.RefreshTokenType)
+ assert.NotEmpty(t, tokenExchangeResponse.AccessToken, "access token")
+ assert.NotEmpty(t, tokenExchangeResponse.RefreshToken, "refresh token")
+ assert.Equal(t, []string(tokenExchangeResponse.Scopes), []string{"profile", "custom_scope:impersonate:id2"})
+
+ t.Log("------ end session (logout) ------")
+
+ newLoc, err := rp.EndSession(provider, idToken, "", "")
+ require.NoError(t, err, "logout")
+ if newLoc != nil {
+ t.Logf("redirect to %s", newLoc)
+ } else {
+ t.Logf("no redirect")
+ }
+
+ t.Log("------- attempt exchage again (should fail) ------")
+
+ tokenExchangeResponse, err = tokenexchange.ExchangeToken(
+ resourceServer,
+ refreshToken,
+ oidc.RefreshTokenType,
+ "",
+ "",
+ []string{},
+ []string{},
+ []string{"profile", "custom_scope:impersonate:id2"},
+ oidc.RefreshTokenType,
+ )
+ require.Error(t, err, "refresh token")
+ assert.Contains(t, err.Error(), "subject_token is invalid")
+ require.Nil(t, tokenExchangeResponse, "token exchange response")
+
+}
+
+func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, clientSecret string) (provider rp.RelyingParty, accessToken, refreshToken, idToken string) {
targetURL := "http://local-site"
localURL, err := url.Parse(targetURL + "/login?requestID=1234")
require.NoError(t, err, "local url")
- seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
- clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25)
- client := storage.WebClient(clientID, "secret", targetURL)
+ client := storage.WebClient(clientID, clientSecret, targetURL)
storage.RegisterClients(client)
jar, err := cookiejar.New(nil)
@@ -58,10 +165,10 @@ func TestRelyingPartySession(t *testing.T) {
t.Log("------- create RP ------")
key := []byte("test1234test1234")
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
- provider, err := rp.NewRelyingPartyOIDC(
+ provider, err = rp.NewRelyingPartyOIDC(
opServer.URL,
clientID,
- "secret",
+ clientSecret,
targetURL,
[]string{"openid", "email", "profile", "offline_access"},
rp.WithPKCE(cookieHandler),
@@ -70,8 +177,10 @@ func TestRelyingPartySession(t *testing.T) {
rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"),
),
)
+ require.NoError(t, err, "new rp")
t.Log("------- get redirect from local client (rp) to OP ------")
+ seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano()))
state := "state-" + strconv.FormatInt(seed.Int63(), 25)
capturedW := httptest.NewRecorder()
get := httptest.NewRequest("GET", localURL.String(), nil)
@@ -114,7 +223,7 @@ func TestRelyingPartySession(t *testing.T) {
t.Log("------- post to login form, get redirect to OP ------")
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL,
- gosubmit.Set("username", "test-user"),
+ gosubmit.Set("username", "test-user@local-site"),
gosubmit.Set("password", "verysecure"))
t.Logf("Get redirect from %s", postLoginRedirectURL)
@@ -130,19 +239,19 @@ func TestRelyingPartySession(t *testing.T) {
t.Logf("setting cookie %s", cookie)
}
- var accessToken, refreshToken, idToken, email string
- redirect := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
+ var email string
+ 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()
+ email = info.Email
http.Redirect(w, r, targetURL, 302)
}
rp.CodeExchangeHandler(rp.UserinfoCallback(redirect), provider, rp.WithURLParam("custom", "param"))(capturedW, get)
@@ -154,7 +263,6 @@ func TestRelyingPartySession(t *testing.T) {
}
}()
require.Less(t, capturedW.Code, 400, "token exchange response code")
- require.Less(t, capturedW.Code, 400, "token exchange response code")
// TODO: how to check the custom header was sent to the server?
//nolint:bodyclose
@@ -169,43 +277,7 @@ func TestRelyingPartySession(t *testing.T) {
assert.NotEmpty(t, accessToken, "access token")
assert.NotEmpty(t, email, "email")
- t.Log("------- refresh tokens ------")
-
- newTokens, err := rp.RefreshAccessToken(provider, refreshToken, "", "")
- require.NoError(t, err, "refresh token")
- assert.NotNil(t, newTokens, "access token")
- t.Logf("new access token %s", newTokens.AccessToken)
- t.Logf("new refresh token %s", newTokens.RefreshToken)
- t.Logf("new token type %s", newTokens.TokenType)
- t.Logf("new expiry %s", newTokens.Expiry.Format(time.RFC3339))
- require.NotEmpty(t, newTokens.AccessToken, "new accessToken")
-
- t.Log("------ end session (logout) ------")
-
- newLoc, err := rp.EndSession(provider, idToken, "", "")
- require.NoError(t, err, "logout")
- if newLoc != nil {
- t.Logf("redirect to %s", newLoc)
- } else {
- t.Logf("no redirect")
- }
-
- t.Log("------ attempt refresh again (should fail) ------")
- t.Log("trying original refresh token", refreshToken)
- _, err = rp.RefreshAccessToken(provider, refreshToken, "", "")
- assert.Errorf(t, err, "refresh with original")
- if newTokens.RefreshToken != "" {
- t.Log("trying replacement refresh token", newTokens.RefreshToken)
- _, err = rp.RefreshAccessToken(provider, newTokens.RefreshToken, "", "")
- assert.Errorf(t, err, "refresh with replacement")
- }
-
- t.Run("WithPrompt", func(t *testing.T) {
- opts := rp.WithPrompt("foo", "bar")()
- url := provider.OAuthConfig().AuthCodeURL("some", opts...)
-
- require.Contains(t, url, "prompt=foo+bar")
- })
+ return provider, accessToken, refreshToken, idToken
}
type deferredHandler struct {
diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go
index a711de9..1686de6 100644
--- a/pkg/client/jwt_profile.go
+++ b/pkg/client/jwt_profile.go
@@ -5,8 +5,8 @@ import (
"golang.org/x/oauth2"
- "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
// JWTProfileExchange handles the oauth2 jwt profile exchange
diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go
index b29fcaa..a934f7d 100644
--- a/pkg/client/profile/jwt_profile.go
+++ b/pkg/client/profile/jwt_profile.go
@@ -7,8 +7,8 @@ import (
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/client"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
// jwtProfileTokenSource implement the oauth2.TokenSource
diff --git a/pkg/client/rp/cli/cli.go b/pkg/client/rp/cli/cli.go
index 6e30e4e..91b200d 100644
--- a/pkg/client/rp/cli/cli.go
+++ b/pkg/client/rp/cli/cli.go
@@ -4,22 +4,22 @@ import (
"context"
"net/http"
- "github.com/zitadel/oidc/pkg/client/rp"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client/rp"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
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/delegation.go b/pkg/client/rp/delegation.go
index a2b1f00..b16a39e 100644
--- a/pkg/client/rp/delegation.go
+++ b/pkg/client/rp/delegation.go
@@ -1,13 +1,13 @@
package rp
import (
- "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange"
+ "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
)
// DelegationTokenRequest is an implementation of TokenExchangeRequest
// it exchanges an "urn:ietf:params:oauth:token-type:access_token" with an optional
-//"urn:ietf:params:oauth:token-type:access_token" actor token for an
-//"urn:ietf:params:oauth:token-type:access_token" delegation token
+// "urn:ietf:params:oauth:token-type:access_token" actor token for an
+// "urn:ietf:params:oauth:token-type:access_token" delegation token
func DelegationTokenRequest(subjectToken string, opts ...tokenexchange.TokenExchangeOption) *tokenexchange.TokenExchangeRequest {
return tokenexchange.NewTokenExchangeRequest(subjectToken, tokenexchange.AccessTokenType, opts...)
}
diff --git a/pkg/client/rp/device.go b/pkg/client/rp/device.go
new file mode 100644
index 0000000..73b67ca
--- /dev/null
+++ b/pkg/client/rp/device.go
@@ -0,0 +1,62 @@
+package rp
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/zitadel/oidc/v2/pkg/client"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+)
+
+func newDeviceClientCredentialsRequest(scopes []string, rp RelyingParty) (*oidc.ClientCredentialsRequest, error) {
+ confg := rp.OAuthConfig()
+ req := &oidc.ClientCredentialsRequest{
+ GrantType: oidc.GrantTypeDeviceCode,
+ Scope: scopes,
+ ClientID: confg.ClientID,
+ ClientSecret: confg.ClientSecret,
+ }
+
+ if signer := rp.Signer(); signer != nil {
+ assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, signer)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build assertion: %w", err)
+ }
+ req.ClientAssertion = assertion
+ req.ClientAssertionType = oidc.ClientAssertionTypeJWTAssertion
+ }
+
+ return req, nil
+}
+
+// DeviceAuthorization starts a new Device Authorization flow as defined
+// in RFC 8628, section 3.1 and 3.2:
+// https://www.rfc-editor.org/rfc/rfc8628#section-3.1
+func DeviceAuthorization(scopes []string, rp RelyingParty) (*oidc.DeviceAuthorizationResponse, error) {
+ req, err := newDeviceClientCredentialsRequest(scopes, rp)
+ if err != nil {
+ return nil, err
+ }
+
+ return client.CallDeviceAuthorizationEndpoint(req, rp)
+}
+
+// DeviceAccessToken attempts to obtain tokens from a Device Authorization,
+// by means of polling as defined in RFC, section 3.3 and 3.4:
+// https://www.rfc-editor.org/rfc/rfc8628#section-3.4
+func DeviceAccessToken(ctx context.Context, deviceCode string, interval time.Duration, rp RelyingParty) (resp *oidc.AccessTokenResponse, err error) {
+ req := &client.DeviceAccessTokenRequest{
+ DeviceAccessTokenRequest: oidc.DeviceAccessTokenRequest{
+ GrantType: oidc.GrantTypeDeviceCode,
+ DeviceCode: deviceCode,
+ },
+ }
+
+ req.ClientCredentialsRequest, err = newDeviceClientCredentialsRequest(nil, rp)
+ if err != nil {
+ return nil, err
+ }
+
+ return client.PollDeviceAccessTokenEndpoint(ctx, interval, req, tokenEndpointCaller{rp})
+}
diff --git a/pkg/client/rp/jwks.go b/pkg/client/rp/jwks.go
index cc49eb7..3438bd6 100644
--- a/pkg/client/rp/jwks.go
+++ b/pkg/client/rp/jwks.go
@@ -9,8 +9,8 @@ import (
"gopkg.in/square/go-jose.v2"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {
diff --git a/pkg/client/rp/mock/generate.go b/pkg/client/rp/mock/generate.go
deleted file mode 100644
index 1e05701..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/pkg/rp Verifier
diff --git a/pkg/client/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go
deleted file mode 100644
index b20db68..0000000
--- a/pkg/client/rp/mock/verifier.mock.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/rp (interfaces: Verifier)
-
-// Package mock is a generated GoMock package.
-package mock
-
-import (
- "context"
- "reflect"
-
- "github.com/golang/mock/gomock"
-
- "github.com/zitadel/oidc/pkg/oidc"
-)
-
-// MockVerifier is a mock of Verifier interface
-type MockVerifier struct {
- ctrl *gomock.Controller
- recorder *MockVerifierMockRecorder
-}
-
-// MockVerifierMockRecorder is the mock recorder for MockVerifier
-type MockVerifierMockRecorder struct {
- mock *MockVerifier
-}
-
-// NewMockVerifier creates a new mock instance
-func NewMockVerifier(ctrl *gomock.Controller) *MockVerifier {
- mock := &MockVerifier{ctrl: ctrl}
- mock.recorder = &MockVerifierMockRecorder{mock}
- return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use
-func (m *MockVerifier) EXPECT() *MockVerifierMockRecorder {
- return m.recorder
-}
-
-// Verify mocks base method
-func (m *MockVerifier) Verify(arg0 context.Context, arg1, arg2 string) (*oidc.IDTokenClaims, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2)
- ret0, _ := ret[0].(*oidc.IDTokenClaims)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Verify indicates an expected call of Verify
-func (mr *MockVerifierMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockVerifier)(nil).Verify), arg0, arg1, arg2)
-}
-
-// VerifyIDToken mocks base method
-func (m *MockVerifier) VerifyIDToken(arg0 context.Context, arg1 string) (*oidc.IDTokenClaims, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
- ret0, _ := ret[0].(*oidc.IDTokenClaims)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// VerifyIDToken indicates an expected call of VerifyIDToken
-func (mr *MockVerifierMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockVerifier)(nil).VerifyIDToken), arg0, arg1)
-}
diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go
index 3758601..ede7453 100644
--- a/pkg/client/rp/relying_party.go
+++ b/pkg/client/rp/relying_party.go
@@ -14,9 +14,9 @@ import (
"golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/client"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
const (
@@ -54,11 +54,15 @@ type RelyingParty interface {
GetEndSessionEndpoint() string
// GetRevokeEndpoint returns the endpoint to revoke a specific token
- // "GetRevokeEndpoint() string" will be added in a future release
+ GetRevokeEndpoint() string
// UserinfoEndpoint returns the userinfo
UserinfoEndpoint() string
+ // GetDeviceAuthorizationEndpoint returns the enpoint which can
+ // be used to start a DeviceAuthorization flow.
+ GetDeviceAuthorizationEndpoint() string
+
// IDTokenVerifier returns the verifier interface used for oidc id_token verification
IDTokenVerifier() IDTokenVerifier
// ErrorHandler returns the handler used for callback errors
@@ -121,6 +125,10 @@ func (rp *relyingParty) UserinfoEndpoint() string {
return rp.endpoints.UserinfoURL
}
+func (rp *relyingParty) GetDeviceAuthorizationEndpoint() string {
+ return rp.endpoints.DeviceAuthorizationURL
+}
+
func (rp *relyingParty) GetEndSessionEndpoint() string {
return rp.endpoints.EndSessionURL
}
@@ -371,7 +379,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 {
@@ -384,7 +392,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)
@@ -392,21 +400,21 @@ 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.
// Custom paramaters can optionally be set to the token URL.
-func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
+func CodeExchangeHandler[C oidc.IDClaims](callback CodeExchangeCallback[C], rp RelyingParty, urlParam ...URLParamOpt) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state, err := tryReadStateCookie(w, r, rp)
if err != nil {
@@ -439,7 +447,7 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlPara
}
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
@@ -448,13 +456,13 @@ func CodeExchangeHandler(callback CodeExchangeCallback, rp RelyingParty, urlPara
}
}
-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)
@@ -465,17 +473,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
@@ -506,11 +514,12 @@ type OptionFunc func(RelyingParty)
type Endpoints struct {
oauth2.Endpoint
- IntrospectURL string
- UserinfoURL string
- JKWsURL string
- EndSessionURL string
- RevokeURL string
+ IntrospectURL string
+ UserinfoURL string
+ JKWsURL string
+ EndSessionURL string
+ RevokeURL string
+ DeviceAuthorizationURL string
}
func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
@@ -520,11 +529,12 @@ func GetEndpoints(discoveryConfig *oidc.DiscoveryConfiguration) Endpoints {
AuthStyle: oauth2.AuthStyleAutoDetect,
TokenURL: discoveryConfig.TokenEndpoint,
},
- IntrospectURL: discoveryConfig.IntrospectionEndpoint,
- UserinfoURL: discoveryConfig.UserinfoEndpoint,
- JKWsURL: discoveryConfig.JwksURI,
- EndSessionURL: discoveryConfig.EndSessionEndpoint,
- RevokeURL: discoveryConfig.RevocationEndpoint,
+ IntrospectURL: discoveryConfig.IntrospectionEndpoint,
+ UserinfoURL: discoveryConfig.UserinfoEndpoint,
+ JKWsURL: discoveryConfig.JwksURI,
+ EndSessionURL: discoveryConfig.EndSessionEndpoint,
+ RevokeURL: discoveryConfig.RevocationEndpoint,
+ DeviceAuthorizationURL: discoveryConfig.DeviceAuthorizationEndpoint,
}
}
diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go
index 3950fe1..c1ac88d 100644
--- a/pkg/client/rp/tockenexchange.go
+++ b/pkg/client/rp/tockenexchange.go
@@ -5,7 +5,7 @@ import (
"golang.org/x/oauth2"
- "github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange"
+ "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
)
// TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`
diff --git a/pkg/client/rp/verifier.go b/pkg/client/rp/verifier.go
index 6b3b3fd..75d149b 100644
--- a/pkg/client/rp/verifier.go
+++ b/pkg/client/rp/verifier.go
@@ -6,7 +6,7 @@ import (
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type IDTokenVerifier interface {
@@ -20,76 +20,78 @@ 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)
+// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponseValidation
+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()
+// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+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
}
// VerifyAccessToken validates the access token according to
-//https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
+// https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
func VerifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
if atHash == "" {
return 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 b1bc47e..4e0353c 100644
--- a/pkg/client/rs/resource_server.go
+++ b/pkg/client/rs/resource_server.go
@@ -6,13 +6,14 @@ import (
"net/http"
"time"
- "github.com/zitadel/oidc/pkg/client"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/client"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type ResourceServer interface {
IntrospectionURL() string
+ TokenEndpoint() string
HttpClient() *http.Client
AuthFn() (interface{}, error)
}
@@ -29,6 +30,10 @@ func (r *resourceServer) IntrospectionURL() string {
return r.introspectURL
}
+func (r *resourceServer) TokenEndpoint() string {
+ return r.tokenURL
+}
+
func (r *resourceServer) HttpClient() *http.Client {
return r.httpClient
}
@@ -107,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
@@ -116,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/client/tokenexchange/tokenexchange.go b/pkg/client/tokenexchange/tokenexchange.go
new file mode 100644
index 0000000..1375f68
--- /dev/null
+++ b/pkg/client/tokenexchange/tokenexchange.go
@@ -0,0 +1,127 @@
+package tokenexchange
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/zitadel/oidc/v2/pkg/client"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+)
+
+type TokenExchanger interface {
+ TokenEndpoint() string
+ HttpClient() *http.Client
+ AuthFn() (interface{}, error)
+}
+
+type OAuthTokenExchange struct {
+ httpClient *http.Client
+ tokenEndpoint string
+ authFn func() (interface{}, error)
+}
+
+func NewTokenExchanger(issuer string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
+ return newOAuthTokenExchange(issuer, nil, options...)
+}
+
+func NewTokenExchangerClientCredentials(issuer, clientID, clientSecret string, options ...func(source *OAuthTokenExchange)) (TokenExchanger, error) {
+ authorizer := func() (interface{}, error) {
+ return httphelper.AuthorizeBasic(clientID, clientSecret), nil
+ }
+ return newOAuthTokenExchange(issuer, authorizer, options...)
+}
+
+func newOAuthTokenExchange(issuer string, authorizer func() (interface{}, error), options ...func(source *OAuthTokenExchange)) (*OAuthTokenExchange, error) {
+ te := &OAuthTokenExchange{
+ httpClient: httphelper.DefaultHTTPClient,
+ }
+ for _, opt := range options {
+ opt(te)
+ }
+
+ if te.tokenEndpoint == "" {
+ config, err := client.Discover(issuer, te.httpClient)
+ if err != nil {
+ return nil, err
+ }
+
+ te.tokenEndpoint = config.TokenEndpoint
+ }
+
+ if te.tokenEndpoint == "" {
+ return nil, errors.New("tokenURL is empty: please provide with either `WithStaticTokenEndpoint` or a discovery url")
+ }
+
+ te.authFn = authorizer
+
+ return te, nil
+}
+
+func WithHTTPClient(client *http.Client) func(*OAuthTokenExchange) {
+ return func(source *OAuthTokenExchange) {
+ source.httpClient = client
+ }
+}
+
+func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*OAuthTokenExchange) {
+ return func(source *OAuthTokenExchange) {
+ source.tokenEndpoint = tokenEndpoint
+ }
+}
+
+func (te *OAuthTokenExchange) TokenEndpoint() string {
+ return te.tokenEndpoint
+}
+
+func (te *OAuthTokenExchange) HttpClient() *http.Client {
+ return te.httpClient
+}
+
+func (te *OAuthTokenExchange) AuthFn() (interface{}, error) {
+ if te.authFn != nil {
+ return te.authFn()
+ }
+
+ return nil, nil
+}
+
+// ExchangeToken sends a token exchange request (rfc 8693) to te's token endpoint.
+// SubjectToken and SubjectTokenType are required parameters.
+func ExchangeToken(
+ te TokenExchanger,
+ SubjectToken string,
+ SubjectTokenType oidc.TokenType,
+ ActorToken string,
+ ActorTokenType oidc.TokenType,
+ Resource []string,
+ Audience []string,
+ Scopes []string,
+ RequestedTokenType oidc.TokenType,
+) (*oidc.TokenExchangeResponse, error) {
+ if SubjectToken == "" {
+ return nil, errors.New("empty subject_token")
+ }
+ if SubjectTokenType == "" {
+ return nil, errors.New("empty subject_token_type")
+ }
+
+ authFn, err := te.AuthFn()
+ if err != nil {
+ return nil, err
+ }
+
+ request := oidc.TokenExchangeRequest{
+ GrantType: oidc.GrantTypeTokenExchange,
+ SubjectToken: SubjectToken,
+ SubjectTokenType: SubjectTokenType,
+ ActorToken: ActorToken,
+ ActorTokenType: ActorTokenType,
+ Resource: Resource,
+ Audience: Audience,
+ Scopes: Scopes,
+ RequestedTokenType: RequestedTokenType,
+ }
+
+ return client.CallTokenExchangeEndpoint(request, authFn, te)
+}
diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go
index e1e459c..37c1783 100644
--- a/pkg/oidc/code_challenge.go
+++ b/pkg/oidc/code_challenge.go
@@ -3,7 +3,7 @@ package oidc
import (
"crypto/sha256"
- "github.com/zitadel/oidc/pkg/crypto"
+ "github.com/zitadel/oidc/v2/pkg/crypto"
)
const (
diff --git a/pkg/oidc/device_authorization.go b/pkg/oidc/device_authorization.go
new file mode 100644
index 0000000..68b8efa
--- /dev/null
+++ b/pkg/oidc/device_authorization.go
@@ -0,0 +1,29 @@
+package oidc
+
+// DeviceAuthorizationRequest implements
+// https://www.rfc-editor.org/rfc/rfc8628#section-3.1,
+// 3.1 Device Authorization Request.
+type DeviceAuthorizationRequest struct {
+ Scopes SpaceDelimitedArray `schema:"scope"`
+ ClientID string `schema:"client_id"`
+}
+
+// DeviceAuthorizationResponse implements
+// https://www.rfc-editor.org/rfc/rfc8628#section-3.2
+// 3.2. Device Authorization Response.
+type DeviceAuthorizationResponse struct {
+ DeviceCode string `json:"device_code"`
+ UserCode string `json:"user_code"`
+ VerificationURI string `json:"verification_uri"`
+ VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
+ ExpiresIn int `json:"expires_in"`
+ Interval int `json:"interval,omitempty"`
+}
+
+// DeviceAccessTokenRequest implements
+// https://www.rfc-editor.org/rfc/rfc8628#section-3.4,
+// Device Access Token Request.
+type DeviceAccessTokenRequest struct {
+ GrantType GrantType `json:"grant_type" schema:"grant_type"`
+ DeviceCode string `json:"device_code" schema:"device_code"`
+}
diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go
index fbc417b..3574101 100644
--- a/pkg/oidc/discovery.go
+++ b/pkg/oidc/discovery.go
@@ -30,6 +30,8 @@ type DiscoveryConfiguration struct {
// EndSessionEndpoint is a URL where the RP can perform a redirect to request that the End-User be logged out at the OP.
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
+ DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint,omitempty"`
+
// CheckSessionIframe is a URL where the OP provides an iframe that support cross-origin communications for session state information with the RP Client.
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
diff --git a/pkg/oidc/error.go b/pkg/oidc/error.go
index 5797a59..79acecd 100644
--- a/pkg/oidc/error.go
+++ b/pkg/oidc/error.go
@@ -18,6 +18,14 @@ const (
InteractionRequired errorType = "interaction_required"
LoginRequired errorType = "login_required"
RequestNotSupported errorType = "request_not_supported"
+
+ // Additional error codes as defined in
+ // https://www.rfc-editor.org/rfc/rfc8628#section-3.5
+ // Device Access Token Response
+ AuthorizationPending errorType = "authorization_pending"
+ SlowDown errorType = "slow_down"
+ AccessDenied errorType = "access_denied"
+ ExpiredToken errorType = "expired_token"
)
var (
@@ -77,6 +85,32 @@ var (
ErrorType: RequestNotSupported,
}
}
+
+ // Device Access Token errors:
+ ErrAuthorizationPending = func() *Error {
+ return &Error{
+ ErrorType: AuthorizationPending,
+ Description: "The client SHOULD repeat the access token request to the token endpoint, after interval from device authorization response.",
+ }
+ }
+ ErrSlowDown = func() *Error {
+ return &Error{
+ ErrorType: SlowDown,
+ Description: "Polling should continue, but the interval MUST be increased by 5 seconds for this and all subsequent requests.",
+ }
+ }
+ ErrAccessDenied = func() *Error {
+ return &Error{
+ ErrorType: AccessDenied,
+ Description: "The authorization request was denied.",
+ }
+ }
+ ErrExpiredDeviceCode = func() *Error {
+ return &Error{
+ ErrorType: ExpiredToken,
+ Description: "The \"device_code\" has expired.",
+ }
+ }
)
type Error struct {
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..b63bf30
--- /dev/null
+++ b/pkg/oidc/regression_data/oidc.AccessTokenClaims.json
@@ -0,0 +1,23 @@
+{
+ "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 fb87e13..b017023 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/pkg/crypto"
- "github.com/zitadel/oidc/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/crypto"
)
const (
@@ -20,374 +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{})
+// 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"`
+ AuthTime Time `json:"auth_time,omitempty"`
+ NotBefore Time `json:"nbf,omitempty"`
+ Nonce string `json:"nonce,omitempty"`
+ AuthenticationContextClassReference string `json:"acr,omitempty"`
+ AuthenticationMethodsReferences []string `json:"amr,omitempty"`
+ AuthorizedParty string `json:"azp,omitempty"`
+ ClientID string `json:"client_id,omitempty"`
+ JWTID string `json:"jti,omitempty"`
+
+ // Additional information set by this framework
+ SignatureAlg jose.SignatureAlgorithm `json:"-"`
}
-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 (c *TokenClaims) GetIssuer() string {
+ return c.Issuer
}
-func EmptyAccessTokenClaims() AccessTokenClaims {
- return new(accessTokenClaims)
+func (c *TokenClaims) GetSubject() string {
+ return c.Subject
}
-func NewAccessTokenClaims(issuer, subject string, audience []string, expiration time.Time, id, clientID string, skew time.Duration) AccessTokenClaims {
+func (c *TokenClaims) GetAudience() []string {
+ return c.Audience
+}
+
+func (c *TokenClaims) GetExpiration() time.Time {
+ return c.Expiration.AsTime()
+}
+
+func (c *TokenClaims) GetIssuedAt() time.Time {
+ return c.IssuedAt.AsTime()
+}
+
+func (c *TokenClaims) GetNonce() string {
+ return c.Nonce
+}
+
+func (c *TokenClaims) GetAuthTime() time.Time {
+ return c.AuthTime.AsTime()
+}
+
+func (c *TokenClaims) GetAuthorizedParty() string {
+ return c.AuthorizedParty
+}
+
+func (c *TokenClaims) GetSignatureAlgorithm() jose.SignatureAlgorithm {
+ return c.SignatureAlg
+}
+
+func (c *TokenClaims) GetAuthenticationContextClassReference() string {
+ return c.AuthenticationContextClassReference
+}
+
+func (c *TokenClaims) SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm) {
+ c.SignatureAlg = algorithm
+}
+
+type AccessTokenClaims struct {
+ TokenClaims
+ Scopes SpaceDelimitedArray `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{
- Issuer: issuer,
- Subject: subject,
- Audience: audience,
- Expiration: Time(expiration),
- IssuedAt: Time(now),
- NotBefore: Time(now),
- JWTID: id,
+ return &AccessTokenClaims{
+ TokenClaims: TokenClaims{
+ Issuer: issuer,
+ Subject: subject,
+ Audience: audience,
+ Expiration: FromTime(expiration),
+ IssuedAt: FromTime(now),
+ NotBefore: FromTime(now),
+ JWTID: jwtid,
+ },
}
}
-type accessTokenClaims 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"`
- AuthenticationContextClassReference string `json:"acr,omitempty"`
- AuthenticationMethodsReferences []string `json:"amr,omitempty"`
- SessionID string `json:"sid,omitempty"`
- Scopes SpaceDelimitedArray `json:"scope,omitempty"`
- ClientID string `json:"client_id,omitempty"`
- AccessTokenUseNumber int `json:"at_use_nbr,omitempty"`
+type atcAlias AccessTokenClaims
- claims map[string]interface{} `json:"-"`
- signatureAlg jose.SignatureAlgorithm `json:"-"`
+func (a *AccessTokenClaims) MarshalJSON() ([]byte, error) {
+ return mergeAndMarshalClaims((*atcAlias)(a), a.Claims)
}
-// GetIssuer implements the Claims interface
-func (a *accessTokenClaims) GetIssuer() string {
- return a.Issuer
+func (a *AccessTokenClaims) UnmarshalJSON(data []byte) error {
+ return unmarshalJSONMulti(data, (*atcAlias)(a), &a.Claims)
}
-// 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
-}
-
-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:"-"`
-
- signatureAlg jose.SignatureAlgorithm
-}
-
-// GetIssuer implements the Claims interface
-func (t *idTokenClaims) GetIssuer() string {
- return t.Issuer
-}
-
-// GetAudience implements the Claims interface
-func (t *idTokenClaims) GetAudience() []string {
- return t.Audience
-}
-
-// GetExpiration implements the Claims interface
-func (t *idTokenClaims) GetExpiration() time.Time {
- return time.Time(t.Expiration)
-}
-
-// GetIssuedAt implements the Claims interface
-func (t *idTokenClaims) GetIssuedAt() time.Time {
- return time.Time(t.IssuedAt)
-}
-
-// GetNonce implements the Claims interface
-func (t *idTokenClaims) GetNonce() string {
- return t.Nonce
-}
-
-// GetAuthenticationContextClassReference implements the Claims interface
-func (t *idTokenClaims) GetAuthenticationContextClassReference() string {
- return t.AuthenticationContextClassReference
-}
-
-// GetAuthTime implements the Claims interface
-func (t *idTokenClaims) GetAuthTime() time.Time {
- return time.Time(t.AuthTime)
-}
-
-// GetAuthorizedParty implements the Claims interface
-func (t *idTokenClaims) GetAuthorizedParty() string {
- return t.AuthorizedParty
-}
-
-// SetSignatureAlgorithm implements the Claims interface
-func (t *idTokenClaims) SetSignatureAlgorithm(alg jose.SignatureAlgorithm) {
- t.signatureAlg = alg
-}
-
-// GetNotBefore implements the IDTokenClaims interface
-func (t *idTokenClaims) GetNotBefore() time.Time {
- return time.Time(t.NotBefore)
-}
-
-// GetJWTID implements the IDTokenClaims interface
-func (t *idTokenClaims) GetJWTID() string {
- return t.JWTID
+// 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 {
@@ -399,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"`
@@ -420,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
}
@@ -524,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"`
@@ -549,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 {
@@ -588,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 {
@@ -612,3 +328,12 @@ func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error
}
return signedAssertion.CompactSerialize()
}
+
+type TokenExchangeResponse struct {
+ AccessToken string `json:"access_token"` // Can be access token or ID token
+ IssuedTokenType TokenType `json:"issued_token_type"`
+ TokenType string `json:"token_type"`
+ ExpiresIn uint64 `json:"expires_in,omitempty"`
+ Scopes SpaceDelimitedArray `json:"scope,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty"`
+}
diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go
index ec11057..e63e0e5 100644
--- a/pkg/oidc/token_request.go
+++ b/pkg/oidc/token_request.go
@@ -27,6 +27,9 @@ const (
// GrantTypeImplicit defines the grant type `implicit` used for implicit flows that skip the generation and exchange of an Authorization Code
GrantTypeImplicit GrantType = "implicit"
+ // GrantTypeDeviceCode
+ GrantTypeDeviceCode GrantType = "urn:ietf:params:oauth:grant-type:device_code"
+
// ClientAssertionTypeJWTAssertion defines the client_assertion_type `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`
// used for the OAuth JWT Profile Client Authentication
ClientAssertionTypeJWTAssertion = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
@@ -35,11 +38,34 @@ const (
var AllGrantTypes = []GrantType{
GrantTypeCode, GrantTypeRefreshToken, GrantTypeClientCredentials,
GrantTypeBearer, GrantTypeTokenExchange, GrantTypeImplicit,
- ClientAssertionTypeJWTAssertion,
+ GrantTypeDeviceCode, ClientAssertionTypeJWTAssertion,
}
type GrantType string
+const (
+ AccessTokenType TokenType = "urn:ietf:params:oauth:token-type:access_token"
+ RefreshTokenType TokenType = "urn:ietf:params:oauth:token-type:refresh_token"
+ IDTokenType TokenType = "urn:ietf:params:oauth:token-type:id_token"
+ JWTTokenType TokenType = "urn:ietf:params:oauth:token-type:jwt"
+)
+
+var AllTokenTypes = []TokenType{
+ AccessTokenType, RefreshTokenType, IDTokenType, JWTTokenType,
+}
+
+type TokenType string
+
+func (t TokenType) IsSupported() bool {
+ for _, tt := range AllTokenTypes {
+ if t == tt {
+ return true
+ }
+ }
+
+ return false
+}
+
type TokenRequest interface {
// GrantType GrantType `schema:"grant_type"`
GrantType() GrantType
@@ -161,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
@@ -203,19 +229,22 @@ func (j *JWTTokenRequest) GetScopes() []string {
}
type TokenExchangeRequest struct {
- subjectToken string `schema:"subject_token"`
- subjectTokenType string `schema:"subject_token_type"`
- actorToken string `schema:"actor_token"`
- actorTokenType string `schema:"actor_token_type"`
- resource []string `schema:"resource"`
- audience Audience `schema:"audience"`
- Scope SpaceDelimitedArray `schema:"scope"`
- requestedTokenType string `schema:"requested_token_type"`
+ GrantType GrantType `schema:"grant_type"`
+ SubjectToken string `schema:"subject_token"`
+ SubjectTokenType TokenType `schema:"subject_token_type"`
+ ActorToken string `schema:"actor_token"`
+ ActorTokenType TokenType `schema:"actor_token_type"`
+ Resource []string `schema:"resource"`
+ Audience Audience `schema:"audience"`
+ Scopes SpaceDelimitedArray `schema:"scope"`
+ RequestedTokenType TokenType `schema:"requested_token_type"`
}
type ClientCredentialsRequest struct {
- GrantType GrantType `schema:"grant_type"`
- Scope SpaceDelimitedArray `schema:"scope"`
- ClientID string `schema:"client_id"`
- ClientSecret string `schema:"client_secret"`
+ GrantType GrantType `schema:"grant_type"`
+ Scope SpaceDelimitedArray `schema:"scope"`
+ ClientID string `schema:"client_id"`
+ ClientSecret string `schema:"client_secret"`
+ ClientAssertion string `schema:"client_assertion"`
+ ClientAssertionType string `schema:"client_assertion_type"`
}
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 1260798..cb513a0 100644
--- a/pkg/oidc/types.go
+++ b/pkg/oidc/types.go
@@ -4,9 +4,11 @@ import (
"database/sql/driver"
"encoding/json"
"fmt"
+ "reflect"
"strings"
"time"
+ "github.com/gorilla/schema"
"golang.org/x/text/language"
"gopkg.in/square/go-jose.v2"
)
@@ -44,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 {
@@ -125,19 +160,52 @@ func (s SpaceDelimitedArray) Value() (driver.Value, error) {
return strings.Join(s, " "), nil
}
-type Time time.Time
-
-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
+// NewEncoder returns a schema Encoder with
+// a registered encoder for SpaceDelimitedArray.
+func NewEncoder() *schema.Encoder {
+ e := schema.NewEncoder()
+ e.RegisterEncoder(SpaceDelimitedArray{}, func(value reflect.Value) string {
+ return value.Interface().(SpaceDelimitedArray).Encode()
+ })
+ return e
}
-func (t *Time) MarshalJSON() ([]byte, error) {
- return json.Marshal(time.Time(*t).UTC().Unix())
+type Time int64
+
+func (ts Time) AsTime() time.Time {
+ return time.Unix(int64(ts), 0)
+}
+
+func FromTime(tt time.Time) Time {
+ return Time(tt.Unix())
+}
+
+func NowTime() Time {
+ return FromTime(time.Now())
+}
+
+func (ts *Time) UnmarshalJSON(data []byte) error {
+ var v any
+ if err := json.Unmarshal(data, &v); err != nil {
+ return fmt.Errorf("oidc.Time: %w", err)
+ }
+ switch x := v.(type) {
+ case float64:
+ *ts = Time(x)
+ case string:
+ // Compatibility with Auth0:
+ // https://github.com/zitadel/oidc/issues/292
+ tt, err := time.Parse(time.RFC3339, x)
+ if err != nil {
+ return fmt.Errorf("oidc.Time: %w", err)
+ }
+ *ts = FromTime(tt)
+ case nil:
+ *ts = 0
+ default:
+ return fmt.Errorf("oidc.Time: unable to parse type %T with value %v", x, x)
+ }
+ return nil
}
type RequestObject struct {
@@ -150,5 +218,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 6c62c40..2721e0b 100644
--- a/pkg/oidc/types_test.go
+++ b/pkg/oidc/types_test.go
@@ -3,11 +3,14 @@ package oidc
import (
"bytes"
"encoding/json"
+ "net/url"
"strconv"
"strings"
"testing"
+ "github.com/gorilla/schema"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"golang.org/x/text/language"
)
@@ -109,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
@@ -335,3 +449,74 @@ func TestSpaceDelimitatedArray_ValuerNil(t *testing.T) {
assert.Equal(t, SpaceDelimitedArray(nil), reversed, "scan nil")
}
}
+
+func TestNewEncoder(t *testing.T) {
+ type request struct {
+ Scopes SpaceDelimitedArray `schema:"scope"`
+ }
+ a := request{
+ Scopes: SpaceDelimitedArray{"foo", "bar"},
+ }
+
+ values := make(url.Values)
+ NewEncoder().Encode(a, values)
+ assert.Equal(t, url.Values{"scope": []string{"foo bar"}}, values)
+
+ var b request
+ schema.NewDecoder().Decode(&b, values)
+ assert.Equal(t, a, b)
+}
+
+func TestTime_UnmarshalJSON(t *testing.T) {
+ type dst struct {
+ UpdatedAt Time `json:"updated_at"`
+ }
+ tests := []struct {
+ name string
+ json string
+ want dst
+ wantErr bool
+ }{
+ {
+ name: "RFC3339", // https://github.com/zitadel/oidc/issues/292
+ json: `{"updated_at": "2021-05-11T21:13:25.566Z"}`,
+ want: dst{UpdatedAt: 1620767605},
+ },
+ {
+ name: "int",
+ json: `{"updated_at":1620767605}`,
+ want: dst{UpdatedAt: 1620767605},
+ },
+ {
+ name: "time parse error",
+ json: `{"updated_at":"foo"}`,
+ wantErr: true,
+ },
+ {
+ name: "null",
+ json: `{"updated_at":null}`,
+ },
+ {
+ name: "invalid type",
+ json: `{"updated_at":["foo","bar"]}`,
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var got dst
+ err := json.Unmarshal([]byte(tt.json), &got)
+ if tt.wantErr {
+ assert.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ assert.Equal(t, tt.want, got)
+ })
+ }
+ t.Run("syntax error", func(t *testing.T) {
+ var ts Time
+ err := ts.UnmarshalJSON([]byte{'~'})
+ assert.Error(t, err)
+ })
+}
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 cc18c80..c4ee95e 100644
--- a/pkg/oidc/verifier.go
+++ b/pkg/oidc/verifier.go
@@ -12,7 +12,7 @@ import (
"gopkg.in/square/go-jose.v2"
- str "github.com/zitadel/oidc/pkg/strings"
+ str "github.com/zitadel/oidc/v2/pkg/strings"
)
type Claims interface {
@@ -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 ecfde28..b312098 100644
--- a/pkg/op/auth_request.go
+++ b/pkg/op/auth_request.go
@@ -12,9 +12,9 @@ import (
"github.com/gorilla/mux"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
- str "github.com/zitadel/oidc/pkg/strings"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ str "github.com/zitadel/oidc/v2/pkg/strings"
)
type AuthRequest interface {
@@ -39,10 +39,8 @@ type Authorizer interface {
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
- Signer() Signer
- IDTokenHintVerifier() IDTokenHintVerifier
+ IDTokenHintVerifier(context.Context) IDTokenHintVerifier
Crypto() Crypto
- Issuer() string
RequestObjectSupported() bool
}
@@ -73,8 +71,9 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
}
+ ctx := r.Context()
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() {
- authReq, err = ParseRequestObject(r.Context(), authReq, authorizer.Storage(), authorizer.Issuer())
+ authReq, err = ParseRequestObject(ctx, authReq, authorizer.Storage(), IssuerFromContext(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
@@ -92,7 +91,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
if validater, ok := authorizer.(AuthorizeValidator); ok {
validation = validater.ValidateAuthRequest
}
- userID, err := validation(r.Context(), authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier())
+ userID, err := validation(ctx, authReq, authorizer.Storage(), authorizer.IDTokenHintVerifier(ctx))
if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return
@@ -101,12 +100,12 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
return
}
- req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID)
+ req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
if err != nil {
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
return
}
- client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID())
+ client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
if err != nil {
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder())
return
@@ -390,7 +389,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/auth_request_test.go b/pkg/op/auth_request_test.go
index dc6f655..7a9701b 100644
--- a/pkg/op/auth_request_test.go
+++ b/pkg/op/auth_request_test.go
@@ -13,10 +13,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
- "github.com/zitadel/oidc/pkg/op/mock"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/op/mock"
)
//
diff --git a/pkg/op/client.go b/pkg/op/client.go
index db3d69b..af4724a 100644
--- a/pkg/op/client.go
+++ b/pkg/op/client.go
@@ -1,13 +1,19 @@
package op
import (
+ "context"
+ "errors"
+ "net/http"
+ "net/url"
"time"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
//go:generate go get github.com/dmarkham/enumer
//go:generate go run github.com/dmarkham/enumer -linecomment -sql -json -text -yaml -gqlgen -type=ApplicationType,AccessTokenType
+//go:generate go mod tidy
const (
ApplicationTypeWeb ApplicationType = iota // web
@@ -67,3 +73,95 @@ func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseT
func IsConfidentialType(c Client) bool {
return c.ApplicationType() == ApplicationTypeWeb
}
+
+var (
+ ErrInvalidAuthHeader = errors.New("invalid basic auth header")
+ ErrNoClientCredentials = errors.New("no client credentials provided")
+ ErrMissingClientID = errors.New("client_id missing from request")
+)
+
+type ClientJWTProfile interface {
+ JWTProfileVerifier(context.Context) JWTProfileVerifier
+}
+
+func ClientJWTAuth(ctx context.Context, ca oidc.ClientAssertionParams, verifier ClientJWTProfile) (clientID string, err error) {
+ if ca.ClientAssertion == "" {
+ return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
+ }
+
+ profile, err := VerifyJWTAssertion(ctx, ca.ClientAssertion, verifier.JWTProfileVerifier(ctx))
+ if err != nil {
+ return "", oidc.ErrUnauthorizedClient().WithParent(err).WithDescription("JWT assertion failed")
+ }
+ return profile.Issuer, nil
+}
+
+func ClientBasicAuth(r *http.Request, storage Storage) (clientID string, err error) {
+ clientID, clientSecret, ok := r.BasicAuth()
+ if !ok {
+ return "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
+ }
+ clientID, err = url.QueryUnescape(clientID)
+ if err != nil {
+ return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
+ }
+ clientSecret, err = url.QueryUnescape(clientSecret)
+ if err != nil {
+ return "", oidc.ErrInvalidClient().WithParent(ErrInvalidAuthHeader)
+ }
+ if err := storage.AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
+ return "", oidc.ErrUnauthorizedClient().WithParent(err)
+ }
+ return clientID, nil
+}
+
+type ClientProvider interface {
+ Decoder() httphelper.Decoder
+ Storage() Storage
+}
+
+type clientData struct {
+ ClientID string `schema:"client_id"`
+ oidc.ClientAssertionParams
+}
+
+// ClientIDFromRequest parses the request form and tries to obtain the client ID
+// and reports if it is authenticated, using a JWT or static client secrets over
+// http basic auth.
+//
+// If the Provider implements IntrospectorJWTProfile and "client_assertion" is
+// present in the form data, JWT assertion will be verified and the
+// client ID is taken from there.
+// If any of them is absent, basic auth is attempted.
+// In absence of basic auth data, the unauthenticated client id from the form
+// data is returned.
+//
+// If no client id can be obtained by any method, oidc.ErrInvalidClient
+// is returned with ErrMissingClientID wrapped in it.
+func ClientIDFromRequest(r *http.Request, p ClientProvider) (clientID string, authenticated bool, err error) {
+ err = r.ParseForm()
+ if err != nil {
+ return "", false, oidc.ErrInvalidRequest().WithDescription("cannot parse form").WithParent(err)
+ }
+
+ data := new(clientData)
+ if err = p.Decoder().Decode(data, r.PostForm); err != nil {
+ return "", false, err
+ }
+
+ JWTProfile, ok := p.(ClientJWTProfile)
+ if ok {
+ clientID, err = ClientJWTAuth(r.Context(), data.ClientAssertionParams, JWTProfile)
+ }
+ if !ok || errors.Is(err, ErrNoClientCredentials) {
+ clientID, err = ClientBasicAuth(r, p.Storage())
+ }
+ if err == nil {
+ return clientID, true, nil
+ }
+
+ if data.ClientID == "" {
+ return "", false, oidc.ErrInvalidClient().WithParent(ErrMissingClientID)
+ }
+ return data.ClientID, false, nil
+}
diff --git a/pkg/op/client_test.go b/pkg/op/client_test.go
new file mode 100644
index 0000000..1af4157
--- /dev/null
+++ b/pkg/op/client_test.go
@@ -0,0 +1,253 @@
+package op_test
+
+import (
+ "context"
+ "errors"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/gorilla/schema"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/op/mock"
+)
+
+type testClientJWTProfile struct{}
+
+func (testClientJWTProfile) JWTProfileVerifier(context.Context) op.JWTProfileVerifier { return nil }
+
+func TestClientJWTAuth(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ ca oidc.ClientAssertionParams
+ verifier op.ClientJWTProfile
+ }
+ tests := []struct {
+ name string
+ args args
+ wantClientID string
+ wantErr error
+ }{
+ {
+ name: "empty assertion",
+ args: args{
+ context.Background(),
+ oidc.ClientAssertionParams{},
+ testClientJWTProfile{},
+ },
+ wantErr: op.ErrNoClientCredentials,
+ },
+ {
+ name: "verification error",
+ args: args{
+ context.Background(),
+ oidc.ClientAssertionParams{
+ ClientAssertion: "foo",
+ },
+ testClientJWTProfile{},
+ },
+ wantErr: oidc.ErrParse,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotClientID, err := op.ClientJWTAuth(tt.args.ctx, tt.args.ca, tt.args.verifier)
+ require.ErrorIs(t, err, tt.wantErr)
+ assert.Equal(t, tt.wantClientID, gotClientID)
+ })
+ }
+}
+
+func TestClientBasicAuth(t *testing.T) {
+ errWrong := errors.New("wrong secret")
+
+ type args struct {
+ username string
+ password string
+ }
+ tests := []struct {
+ name string
+ args *args
+ storage op.Storage
+ wantClientID string
+ wantErr error
+ }{
+ {
+ name: "no args",
+ wantErr: op.ErrNoClientCredentials,
+ },
+ {
+ name: "username unescape err",
+ args: &args{
+ username: "%",
+ password: "bar",
+ },
+ wantErr: op.ErrInvalidAuthHeader,
+ },
+ {
+ name: "password unescape err",
+ args: &args{
+ username: "foo",
+ password: "%",
+ },
+ wantErr: op.ErrInvalidAuthHeader,
+ },
+ {
+ name: "auth error",
+ args: &args{
+ username: "foo",
+ password: "wrong",
+ },
+ storage: func() op.Storage {
+ s := mock.NewMockStorage(gomock.NewController(t))
+ s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "wrong").Return(errWrong)
+ return s
+ }(),
+ wantErr: errWrong,
+ },
+ {
+ name: "auth error",
+ args: &args{
+ username: "foo",
+ password: "bar",
+ },
+ storage: func() op.Storage {
+ s := mock.NewMockStorage(gomock.NewController(t))
+ s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
+ return s
+ }(),
+ wantClientID: "foo",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := httptest.NewRequest(http.MethodGet, "/foo", nil)
+ if tt.args != nil {
+ r.SetBasicAuth(tt.args.username, tt.args.password)
+ }
+
+ gotClientID, err := op.ClientBasicAuth(r, tt.storage)
+ require.ErrorIs(t, err, tt.wantErr)
+ assert.Equal(t, tt.wantClientID, gotClientID)
+ })
+ }
+}
+
+type errReader struct{}
+
+func (errReader) Read([]byte) (int, error) {
+ return 0, io.ErrNoProgress
+}
+
+type testClientProvider struct {
+ storage op.Storage
+}
+
+func (testClientProvider) Decoder() httphelper.Decoder {
+ return schema.NewDecoder()
+}
+
+func (p testClientProvider) Storage() op.Storage {
+ return p.storage
+}
+
+func TestClientIDFromRequest(t *testing.T) {
+ type args struct {
+ body io.Reader
+ p op.ClientProvider
+ }
+ type basicAuth struct {
+ username string
+ password string
+ }
+ tests := []struct {
+ name string
+ args args
+ basicAuth *basicAuth
+ wantClientID string
+ wantAuthenticated bool
+ wantErr bool
+ }{
+ {
+ name: "parse error",
+ args: args{
+ body: errReader{},
+ },
+ wantErr: true,
+ },
+ {
+ name: "unauthenticated",
+ args: args{
+ body: strings.NewReader(
+ url.Values{
+ "client_id": []string{"foo"},
+ }.Encode(),
+ ),
+ p: testClientProvider{
+ storage: mock.NewStorage(t),
+ },
+ },
+ wantClientID: "foo",
+ wantAuthenticated: false,
+ },
+ {
+ name: "authenticated",
+ args: args{
+ body: strings.NewReader(
+ url.Values{}.Encode(),
+ ),
+ p: testClientProvider{
+ storage: func() op.Storage {
+ s := mock.NewMockStorage(gomock.NewController(t))
+ s.EXPECT().AuthorizeClientIDSecret(context.Background(), "foo", "bar").Return(nil)
+ return s
+ }(),
+ },
+ },
+ basicAuth: &basicAuth{
+ username: "foo",
+ password: "bar",
+ },
+ wantClientID: "foo",
+ wantAuthenticated: true,
+ },
+ {
+ name: "missing client id",
+ args: args{
+ body: strings.NewReader(
+ url.Values{}.Encode(),
+ ),
+ p: testClientProvider{
+ storage: mock.NewStorage(t),
+ },
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := httptest.NewRequest(http.MethodPost, "/foo", tt.args.body)
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ if tt.basicAuth != nil {
+ r.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
+ }
+
+ gotClientID, gotAuthenticated, err := op.ClientIDFromRequest(r, tt.args.p)
+ if tt.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ assert.Equal(t, tt.wantClientID, gotClientID)
+ assert.Equal(t, tt.wantAuthenticated, gotAuthenticated)
+ })
+ }
+}
diff --git a/pkg/op/config.go b/pkg/op/config.go
index 82cbb47..c40ed39 100644
--- a/pkg/op/config.go
+++ b/pkg/op/config.go
@@ -2,20 +2,24 @@ package op
import (
"errors"
+ "net/http"
"net/url"
- "os"
+ "strings"
"golang.org/x/text/language"
)
-const (
- OidcDevMode = "ZITADEL_OIDC_DEV"
- // deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true)
- devMode = "CAOS_OIDC_DEV"
+var (
+ ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer")
+ ErrInvalidIssuerNoIssuer = errors.New("missing issuer")
+ ErrInvalidIssuerURL = errors.New("invalid url for issuer")
+ ErrInvalidIssuerMissingHost = errors.New("host for issuer missing")
+ ErrInvalidIssuerHTTPS = errors.New("scheme for issuer must be `https`")
)
type Configuration interface {
- Issuer() string
+ IssuerFromRequest(r *http.Request) string
+ Insecure() bool
AuthorizationEndpoint() Endpoint
TokenEndpoint() Endpoint
IntrospectionEndpoint() Endpoint
@@ -23,6 +27,7 @@ type Configuration interface {
RevocationEndpoint() Endpoint
EndSessionEndpoint() Endpoint
KeysEndpoint() Endpoint
+ DeviceAuthorizationEndpoint() Endpoint
AuthMethodPostSupported() bool
CodeMethodS256Supported() bool
@@ -32,6 +37,7 @@ type Configuration interface {
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
GrantTypeClientCredentialsSupported() bool
+ GrantTypeDeviceCodeSupported() bool
IntrospectionAuthMethodPrivateKeyJWTSupported() bool
IntrospectionEndpointSigningAlgorithmsSupported() []string
RevocationAuthMethodPrivateKeyJWTSupported() bool
@@ -40,38 +46,77 @@ type Configuration interface {
RequestObjectSigningAlgorithmsSupported() []string
SupportedUILocales() []language.Tag
+ DeviceAuthorization() DeviceAuthorizationConfig
}
-func ValidateIssuer(issuer string) error {
+type IssuerFromRequest func(r *http.Request) string
+
+func IssuerFromHost(path string) func(bool) (IssuerFromRequest, error) {
+ return func(allowInsecure bool) (IssuerFromRequest, error) {
+ issuerPath, err := url.Parse(path)
+ if err != nil {
+ return nil, ErrInvalidIssuerURL
+ }
+ if err := ValidateIssuerPath(issuerPath); err != nil {
+ return nil, err
+ }
+ return func(r *http.Request) string {
+ return dynamicIssuer(r.Host, path, allowInsecure)
+ }, nil
+ }
+}
+
+func StaticIssuer(issuer string) func(bool) (IssuerFromRequest, error) {
+ return func(allowInsecure bool) (IssuerFromRequest, error) {
+ if err := ValidateIssuer(issuer, allowInsecure); err != nil {
+ return nil, err
+ }
+ return func(_ *http.Request) string {
+ return issuer
+ }, nil
+ }
+}
+
+func ValidateIssuer(issuer string, allowInsecure bool) error {
if issuer == "" {
- return errors.New("missing issuer")
+ return ErrInvalidIssuerNoIssuer
}
u, err := url.Parse(issuer)
if err != nil {
- return errors.New("invalid url for issuer")
+ return ErrInvalidIssuerURL
}
if u.Host == "" {
- return errors.New("host for issuer missing")
+ return ErrInvalidIssuerMissingHost
}
if u.Scheme != "https" {
- if !devLocalAllowed(u) {
- return errors.New("scheme for issuer must be `https`")
+ if !devLocalAllowed(u, allowInsecure) {
+ return ErrInvalidIssuerHTTPS
}
}
- if u.Fragment != "" || len(u.Query()) > 0 {
- return errors.New("no fragments or query allowed for issuer")
+ return ValidateIssuerPath(u)
+}
+
+func ValidateIssuerPath(issuer *url.URL) error {
+ if issuer.Fragment != "" || len(issuer.Query()) > 0 {
+ return ErrInvalidIssuerPath
}
return nil
}
-func devLocalAllowed(url *url.URL) bool {
- _, b := os.LookupEnv(OidcDevMode)
- if !b {
- // check the old / current env var as well
- _, b = os.LookupEnv(devMode)
- if !b {
- return b
- }
+func devLocalAllowed(url *url.URL, allowInsecure bool) bool {
+ if !allowInsecure {
+ return false
}
return url.Scheme == "http"
}
+
+func dynamicIssuer(issuer, path string, allowInsecure bool) string {
+ schema := "https"
+ if allowInsecure {
+ schema = "http"
+ }
+ if len(path) > 0 && !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ return schema + "://" + issuer + path
+}
diff --git a/pkg/op/config_test.go b/pkg/op/config_test.go
index 9ff75f1..cfe4e61 100644
--- a/pkg/op/config_test.go
+++ b/pkg/op/config_test.go
@@ -1,13 +1,17 @@
package op
import (
- "os"
+ "net/http/httptest"
+ "net/url"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
func TestValidateIssuer(t *testing.T) {
type args struct {
- issuer string
+ issuer string
+ allowInsecure bool
}
tests := []struct {
name string
@@ -16,65 +20,97 @@ func TestValidateIssuer(t *testing.T) {
}{
{
"missing issuer fails",
- args{""},
+ args{
+ issuer: "",
+ },
true,
},
{
"invalid url for issuer fails",
- args{":issuer"},
- true,
- },
- {
- "invalid url for issuer fails",
- args{":issuer"},
+ args{
+ issuer: ":issuer",
+ },
true,
},
{
"host for issuer missing fails",
- args{"https:///issuer"},
- true,
- },
- {
- "host for not https fails",
- args{"http://issuer.com"},
+ args{
+ issuer: "https:///issuer",
+ },
true,
},
{
"host with fragment fails",
- args{"https://issuer.com/#issuer"},
+ args{
+ issuer: "https://issuer.com/#issuer",
+ },
true,
},
{
"host with query fails",
- args{"https://issuer.com?issuer=me"},
+ args{
+ issuer: "https://issuer.com?issuer=me",
+ },
+ true,
+ },
+ {
+ "host with http fails",
+ args{
+ issuer: "http://issuer.com",
+ },
true,
},
{
"host with https ok",
- args{"https://issuer.com"},
+ args{
+ issuer: "https://issuer.com",
+ },
false,
},
{
- "localhost with http fails",
- args{"http://localhost:9999"},
+ "custom scheme fails",
+ args{
+ issuer: "custom://localhost:9999",
+ },
+ true,
+ },
+ {
+ "http with allowInsecure ok",
+ args{
+ issuer: "http://localhost:9999",
+ allowInsecure: true,
+ },
+ false,
+ },
+ {
+ "https with allowInsecure ok",
+ args{
+ issuer: "https://localhost:9999",
+ allowInsecure: true,
+ },
+ false,
+ },
+ {
+ "custom scheme with allowInsecure fails",
+ args{
+ issuer: "custom://localhost:9999",
+ allowInsecure: true,
+ },
true,
},
}
- // ensure env is not set
- //nolint:errcheck
- os.Unsetenv(OidcDevMode)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
+ if err := ValidateIssuer(tt.args.issuer, tt.args.allowInsecure); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
-func TestValidateIssuerDevLocalAllowed(t *testing.T) {
+func TestValidateIssuerPath(t *testing.T) {
type args struct {
- issuer string
+ issuerPath *url.URL
}
tests := []struct {
name string
@@ -82,17 +118,217 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
wantErr bool
}{
{
- "localhost with http with dev ok",
- args{"http://localhost:9999"},
+ "empty ok",
+ args{func() *url.URL {
+ u, _ := url.Parse("")
+ return u
+ }()},
false,
},
+ {
+ "custom ok",
+ args{func() *url.URL {
+ u, _ := url.Parse("/custom")
+ return u
+ }()},
+ false,
+ },
+ {
+ "fragment fails",
+ args{func() *url.URL {
+ u, _ := url.Parse("#fragment")
+ return u
+ }()},
+ true,
+ },
+ {
+ "query fails",
+ args{func() *url.URL {
+ u, _ := url.Parse("?query=value")
+ return u
+ }()},
+ true,
+ },
}
- //nolint:errcheck
- os.Setenv(OidcDevMode, "true")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr {
- t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
+ if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr {
+ t.Errorf("ValidateIssuerPath() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestIssuerFromHost(t *testing.T) {
+ type args struct {
+ path string
+ allowInsecure bool
+ target string
+ }
+ type res struct {
+ issuer string
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ res res
+ }{
+ {
+ "invalid issuer path",
+ args{
+ path: "/#fragment",
+ allowInsecure: false,
+ },
+ res{
+ issuer: "",
+ err: ErrInvalidIssuerPath,
+ },
+ },
+ {
+ "empty path secure",
+ args{
+ path: "",
+ allowInsecure: false,
+ target: "https://issuer.com",
+ },
+ res{
+ issuer: "https://issuer.com",
+ err: nil,
+ },
+ },
+ {
+ "custom path secure",
+ args{
+ path: "/custom/",
+ allowInsecure: false,
+ target: "https://issuer.com",
+ },
+ res{
+ issuer: "https://issuer.com/custom/",
+ err: nil,
+ },
+ },
+ {
+ "custom path no leading slash",
+ args{
+ path: "custom/",
+ allowInsecure: false,
+ target: "https://issuer.com",
+ },
+ res{
+ issuer: "https://issuer.com/custom/",
+ err: nil,
+ },
+ },
+ {
+ "empty path unsecure",
+ args{
+ path: "",
+ allowInsecure: true,
+ target: "http://issuer.com",
+ },
+ res{
+ issuer: "http://issuer.com",
+ err: nil,
+ },
+ },
+ {
+ "custom path unsecure",
+ args{
+ path: "/custom/",
+ allowInsecure: true,
+ target: "http://issuer.com",
+ },
+ res{
+ issuer: "http://issuer.com/custom/",
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ issuer, err := IssuerFromHost(tt.args.path)(tt.args.allowInsecure)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ req := httptest.NewRequest("", tt.args.target, nil)
+ assert.Equal(t, tt.res.issuer, issuer(req))
+ }
+ if tt.res.err != nil {
+ assert.ErrorIs(t, err, tt.res.err)
+ }
+ })
+ }
+}
+
+func TestStaticIssuer(t *testing.T) {
+ type args struct {
+ issuer string
+ allowInsecure bool
+ }
+ type res struct {
+ issuer string
+ err error
+ }
+ tests := []struct {
+ name string
+ args args
+ res res
+ }{
+ {
+ "invalid issuer",
+ args{
+ issuer: "",
+ allowInsecure: false,
+ },
+ res{
+ issuer: "",
+ err: ErrInvalidIssuerNoIssuer,
+ },
+ },
+ {
+ "empty path secure",
+ args{
+ issuer: "https://issuer.com",
+ allowInsecure: false,
+ },
+ res{
+ issuer: "https://issuer.com",
+ err: nil,
+ },
+ },
+ {
+ "custom path secure",
+ args{
+ issuer: "https://issuer.com/custom/",
+ allowInsecure: false,
+ },
+ res{
+ issuer: "https://issuer.com/custom/",
+ err: nil,
+ },
+ },
+ {
+ "unsecure",
+ args{
+ issuer: "http://issuer.com",
+ allowInsecure: true,
+ },
+ res{
+ issuer: "http://issuer.com",
+ err: nil,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ issuer, err := StaticIssuer(tt.args.issuer)(tt.args.allowInsecure)
+ if tt.res.err == nil {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.res.issuer, issuer(nil))
+ }
+ if tt.res.err != nil {
+ assert.ErrorIs(t, err, tt.res.err)
}
})
}
diff --git a/pkg/op/context.go b/pkg/op/context.go
new file mode 100644
index 0000000..7cff5a7
--- /dev/null
+++ b/pkg/op/context.go
@@ -0,0 +1,53 @@
+package op
+
+import (
+ "context"
+ "net/http"
+)
+
+type key int
+
+const (
+ issuerKey key = 0
+)
+
+type IssuerInterceptor struct {
+ issuerFromRequest IssuerFromRequest
+}
+
+// NewIssuerInterceptor will set the issuer into the context
+// by the provided IssuerFromRequest (e.g. returned from StaticIssuer or IssuerFromHost)
+func NewIssuerInterceptor(issuerFromRequest IssuerFromRequest) *IssuerInterceptor {
+ return &IssuerInterceptor{
+ issuerFromRequest: issuerFromRequest,
+ }
+}
+
+func (i *IssuerInterceptor) Handler(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ i.setIssuerCtx(w, r, next)
+ })
+}
+
+func (i *IssuerInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ i.setIssuerCtx(w, r, next)
+ }
+}
+
+// IssuerFromContext reads the issuer from the context (set by an IssuerInterceptor)
+// it will return an empty string if not found
+func IssuerFromContext(ctx context.Context) string {
+ ctxIssuer, _ := ctx.Value(issuerKey).(string)
+ return ctxIssuer
+}
+
+// ContextWithIssuer returns a new context with issuer set to it.
+func ContextWithIssuer(ctx context.Context, issuer string) context.Context {
+ return context.WithValue(ctx, issuerKey, issuer)
+}
+
+func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) {
+ r = r.WithContext(ContextWithIssuer(r.Context(), i.issuerFromRequest(r)))
+ next.ServeHTTP(w, r)
+}
diff --git a/pkg/op/context_test.go b/pkg/op/context_test.go
new file mode 100644
index 0000000..e6bfcec
--- /dev/null
+++ b/pkg/op/context_test.go
@@ -0,0 +1,76 @@
+package op
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIssuerInterceptor(t *testing.T) {
+ type fields struct {
+ issuerFromRequest IssuerFromRequest
+ }
+ type args struct {
+ r *http.Request
+ next http.Handler
+ }
+ type res struct {
+ issuer string
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ res res
+ }{
+ {
+ "empty",
+ fields{
+ func(r *http.Request) string {
+ return ""
+ },
+ },
+ args{},
+ res{
+ issuer: "",
+ },
+ },
+ {
+ "static",
+ fields{
+ func(r *http.Request) string {
+ return "static"
+ },
+ },
+ args{},
+ res{
+ issuer: "static",
+ },
+ },
+ {
+ "host",
+ fields{
+ func(r *http.Request) string {
+ return r.Host
+ },
+ },
+ args{},
+ res{
+ issuer: "issuer.com",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ i := NewIssuerInterceptor(tt.fields.issuerFromRequest)
+ next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, tt.res.issuer, IssuerFromContext(r.Context()))
+ })
+ req := httptest.NewRequest("", "https://issuer.com", nil)
+ i.Handler(next).ServeHTTP(nil, req)
+ i.HandlerFunc(next).ServeHTTP(nil, req)
+ })
+ }
+}
diff --git a/pkg/op/crypto.go b/pkg/op/crypto.go
index f14b1de..6786022 100644
--- a/pkg/op/crypto.go
+++ b/pkg/op/crypto.go
@@ -1,7 +1,7 @@
package op
import (
- "github.com/zitadel/oidc/pkg/crypto"
+ "github.com/zitadel/oidc/v2/pkg/crypto"
)
type Crypto interface {
diff --git a/pkg/op/device.go b/pkg/op/device.go
new file mode 100644
index 0000000..04c06f2
--- /dev/null
+++ b/pkg/op/device.go
@@ -0,0 +1,265 @@
+package op
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "math/big"
+ "net/http"
+ "strings"
+ "time"
+
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+)
+
+type DeviceAuthorizationConfig struct {
+ Lifetime time.Duration
+ PollInterval time.Duration
+ UserFormURL string // the URL where the user must go to authorize the device
+ UserCode UserCodeConfig
+}
+
+type UserCodeConfig struct {
+ CharSet string
+ CharAmount int
+ DashInterval int
+}
+
+const (
+ CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ"
+ CharSetDigits = "0123456789"
+)
+
+var (
+ UserCodeBase20 = UserCodeConfig{
+ CharSet: CharSetBase20,
+ CharAmount: 8,
+ DashInterval: 4,
+ }
+ UserCodeDigits = UserCodeConfig{
+ CharSet: CharSetDigits,
+ CharAmount: 9,
+ DashInterval: 3,
+ }
+)
+
+func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if err := DeviceAuthorization(w, r, o); err != nil {
+ RequestError(w, r, err)
+ }
+ }
+}
+
+func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
+ storage, err := assertDeviceStorage(o.Storage())
+ if err != nil {
+ return err
+ }
+
+ req, err := ParseDeviceCodeRequest(r, o)
+ if err != nil {
+ return err
+ }
+
+ config := o.DeviceAuthorization()
+
+ deviceCode, err := NewDeviceCode(RecommendedDeviceCodeBytes)
+ if err != nil {
+ return err
+ }
+ userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
+ if err != nil {
+ return err
+ }
+
+ expires := time.Now().Add(config.Lifetime)
+ err = storage.StoreDeviceAuthorization(r.Context(), req.ClientID, deviceCode, userCode, expires, req.Scopes)
+ if err != nil {
+ return err
+ }
+
+ response := &oidc.DeviceAuthorizationResponse{
+ DeviceCode: deviceCode,
+ UserCode: userCode,
+ VerificationURI: config.UserFormURL,
+ ExpiresIn: int(config.Lifetime / time.Second),
+ Interval: int(config.PollInterval / time.Second),
+ }
+
+ response.VerificationURIComplete = fmt.Sprintf("%s?user_code=%s", config.UserFormURL, userCode)
+
+ httphelper.MarshalJSON(w, response)
+ return nil
+}
+
+func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
+ clientID, _, err := ClientIDFromRequest(r, o)
+ if err != nil {
+ return nil, err
+ }
+
+ req := new(oidc.DeviceAuthorizationRequest)
+ if err := o.Decoder().Decode(req, r.Form); err != nil {
+ return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err)
+ }
+ req.ClientID = clientID
+
+ return req, nil
+}
+
+// 16 bytes gives 128 bit of entropy.
+// results in a 22 character base64 encoded string.
+const RecommendedDeviceCodeBytes = 16
+
+func NewDeviceCode(nBytes int) (string, error) {
+ bytes := make([]byte, nBytes)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", fmt.Errorf("%w getting entropy for device code", err)
+ }
+ return base64.RawURLEncoding.EncodeToString(bytes), nil
+}
+
+func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
+ var buf strings.Builder
+ if dashInterval > 0 {
+ buf.Grow(charAmount + charAmount/dashInterval - 1)
+ } else {
+ buf.Grow(charAmount)
+ }
+
+ max := big.NewInt(int64(len(charSet)))
+
+ for i := 0; i < charAmount; i++ {
+ if dashInterval != 0 && i != 0 && i%dashInterval == 0 {
+ buf.WriteByte('-')
+ }
+
+ bi, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ return "", fmt.Errorf("%w getting entropy for user code", err)
+ }
+
+ buf.WriteRune(charSet[int(bi.Int64())])
+ }
+
+ return buf.String(), nil
+}
+
+type deviceAccessTokenRequest struct {
+ subject string
+ audience []string
+ scopes []string
+}
+
+func (r *deviceAccessTokenRequest) GetSubject() string {
+ return r.subject
+}
+
+func (r *deviceAccessTokenRequest) GetAudience() []string {
+ return r.audience
+}
+
+func (r *deviceAccessTokenRequest) GetScopes() []string {
+ return r.scopes
+}
+
+func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
+ if err := deviceAccessToken(w, r, exchanger); err != nil {
+ RequestError(w, r, err)
+ }
+}
+
+func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error {
+ // use a limited context timeout shorter as the default
+ // poll interval of 5 seconds.
+ ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
+ defer cancel()
+ r = r.WithContext(ctx)
+
+ clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger)
+ if err != nil {
+ return err
+ }
+
+ req, err := ParseDeviceAccessTokenRequest(r, exchanger)
+ if err != nil {
+ return err
+ }
+ state, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
+ if err != nil {
+ return err
+ }
+
+ client, err := exchanger.Storage().GetClientByClientID(ctx, clientID)
+ if err != nil {
+ return err
+ }
+ if clientAuthenticated != IsConfidentialType(client) {
+ return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
+ WithDescription("confidential client requires authentication")
+ }
+
+ tokenRequest := &deviceAccessTokenRequest{
+ subject: state.Subject,
+ audience: []string{clientID},
+ scopes: state.Scopes,
+ }
+ resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
+ if err != nil {
+ return err
+ }
+
+ httphelper.MarshalJSON(w, resp)
+ return nil
+}
+
+func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) {
+ req := new(oidc.DeviceAccessTokenRequest)
+ if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil {
+ return nil, err
+ }
+ return req, nil
+}
+
+func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
+ storage, err := assertDeviceStorage(exchanger.Storage())
+ if err != nil {
+ return nil, err
+ }
+
+ state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
+ if errors.Is(err, context.DeadlineExceeded) {
+ return nil, oidc.ErrSlowDown().WithParent(err)
+ }
+ if err != nil {
+ return nil, oidc.ErrAccessDenied().WithParent(err)
+ }
+ if state.Denied {
+ return state, oidc.ErrAccessDenied()
+ }
+ if state.Done {
+ return state, nil
+ }
+ if time.Now().After(state.Expires) {
+ return state, oidc.ErrExpiredDeviceCode()
+ }
+ return state, oidc.ErrAuthorizationPending()
+}
+
+func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client AccessTokenClient) (*oidc.AccessTokenResponse, error) {
+ accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeBearer, creator, client, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return &oidc.AccessTokenResponse{
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ TokenType: oidc.BearerToken,
+ ExpiresIn: uint64(validity.Seconds()),
+ }, nil
+}
diff --git a/pkg/op/device_test.go b/pkg/op/device_test.go
new file mode 100644
index 0000000..69ba102
--- /dev/null
+++ b/pkg/op/device_test.go
@@ -0,0 +1,407 @@
+package op_test
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "io"
+ mr "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+)
+
+func Test_deviceAuthorizationHandler(t *testing.T) {
+ req := &oidc.DeviceAuthorizationRequest{
+ Scopes: []string{"foo", "bar"},
+ ClientID: "web",
+ }
+ values := make(url.Values)
+ testProvider.Encoder().Encode(req, values)
+ body := strings.NewReader(values.Encode())
+
+ r := httptest.NewRequest(http.MethodPost, "/", body)
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ w := httptest.NewRecorder()
+
+ runWithRandReader(mr.New(mr.NewSource(1)), func() {
+ op.DeviceAuthorizationHandler(testProvider)(w, r)
+ })
+
+ result := w.Result()
+
+ assert.Less(t, result.StatusCode, 300)
+
+ got, _ := io.ReadAll(result.Body)
+ assert.JSONEq(t, `{"device_code":"Uv38ByGCZU8WP18PmmIdcg", "expires_in":300, "interval":5, "user_code":"JKRV-FRGK", "verification_uri":"https://localhost:9998/device", "verification_uri_complete":"https://localhost:9998/device?user_code=JKRV-FRGK"}`, string(got))
+}
+
+func TestParseDeviceCodeRequest(t *testing.T) {
+ tests := []struct {
+ name string
+ req *oidc.DeviceAuthorizationRequest
+ wantErr bool
+ }{
+ {
+ name: "empty request",
+ wantErr: true,
+ },
+ {
+ name: "success",
+ req: &oidc.DeviceAuthorizationRequest{
+ Scopes: oidc.SpaceDelimitedArray{"foo", "bar"},
+ ClientID: "web",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var body io.Reader
+ if tt.req != nil {
+ values := make(url.Values)
+ testProvider.Encoder().Encode(tt.req, values)
+ body = strings.NewReader(values.Encode())
+ }
+
+ r := httptest.NewRequest(http.MethodPost, "/", body)
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ got, err := op.ParseDeviceCodeRequest(r, testProvider)
+ if tt.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ assert.Equal(t, tt.req, got)
+ })
+ }
+}
+
+func runWithRandReader(r io.Reader, f func()) {
+ originalReader := rand.Reader
+ rand.Reader = r
+ defer func() {
+ rand.Reader = originalReader
+ }()
+
+ f()
+}
+
+func TestNewDeviceCode(t *testing.T) {
+ t.Run("reader error", func(t *testing.T) {
+ runWithRandReader(errReader{}, func() {
+ _, err := op.NewDeviceCode(16)
+ require.Error(t, err)
+ })
+ })
+
+ t.Run("different lengths, rand reader", func(t *testing.T) {
+ for i := 1; i <= 32; i++ {
+ got, err := op.NewDeviceCode(i)
+ require.NoError(t, err)
+ assert.Len(t, got, base64.RawURLEncoding.EncodedLen(i))
+ }
+ })
+
+}
+
+func TestNewUserCode(t *testing.T) {
+ type args struct {
+ charset []rune
+ charAmount int
+ dashInterval int
+ }
+ tests := []struct {
+ name string
+ args args
+ reader io.Reader
+ want string
+ wantErr bool
+ }{
+ {
+ name: "reader error",
+ args: args{
+ charset: []rune(op.CharSetBase20),
+ charAmount: 8,
+ dashInterval: 4,
+ },
+ reader: errReader{},
+ wantErr: true,
+ },
+ {
+ name: "base20",
+ args: args{
+ charset: []rune(op.CharSetBase20),
+ charAmount: 8,
+ dashInterval: 4,
+ },
+ reader: mr.New(mr.NewSource(1)),
+ want: "XKCD-HTTD",
+ },
+ {
+ name: "digits",
+ args: args{
+ charset: []rune(op.CharSetDigits),
+ charAmount: 9,
+ dashInterval: 3,
+ },
+ reader: mr.New(mr.NewSource(1)),
+ want: "271-256-225",
+ },
+ {
+ name: "no dashes",
+ args: args{
+ charset: []rune(op.CharSetDigits),
+ charAmount: 9,
+ },
+ reader: mr.New(mr.NewSource(1)),
+ want: "271256225",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ runWithRandReader(tt.reader, func() {
+ got, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
+ if tt.wantErr {
+ require.ErrorIs(t, err, io.ErrNoProgress)
+ } else {
+ require.NoError(t, err)
+ }
+ assert.Equal(t, tt.want, got)
+ })
+
+ })
+ }
+
+ t.Run("crypto/rand", func(t *testing.T) {
+ const testN = 100000
+
+ for _, c := range []op.UserCodeConfig{op.UserCodeBase20, op.UserCodeDigits} {
+ t.Run(c.CharSet, func(t *testing.T) {
+ results := make(map[string]int)
+
+ for i := 0; i < testN; i++ {
+ code, err := op.NewUserCode([]rune(c.CharSet), c.CharAmount, c.DashInterval)
+ require.NoError(t, err)
+ results[code]++
+ }
+
+ t.Log(results)
+
+ var duplicates int
+ for code, count := range results {
+ assert.Less(t, count, 3, code)
+ if count == 2 {
+ duplicates++
+ }
+ }
+
+ })
+ }
+ })
+}
+
+func BenchmarkNewUserCode(b *testing.B) {
+ type args struct {
+ charset []rune
+ charAmount int
+ dashInterval int
+ }
+ tests := []struct {
+ name string
+ args args
+ reader io.Reader
+ }{
+ {
+ name: "math rand, base20",
+ args: args{
+ charset: []rune(op.CharSetBase20),
+ charAmount: 8,
+ dashInterval: 4,
+ },
+ reader: mr.New(mr.NewSource(1)),
+ },
+ {
+ name: "math rand, digits",
+ args: args{
+ charset: []rune(op.CharSetDigits),
+ charAmount: 9,
+ dashInterval: 3,
+ },
+ reader: mr.New(mr.NewSource(1)),
+ },
+ {
+ name: "crypto rand, base20",
+ args: args{
+ charset: []rune(op.CharSetBase20),
+ charAmount: 8,
+ dashInterval: 4,
+ },
+ reader: rand.Reader,
+ },
+ {
+ name: "crypto rand, digits",
+ args: args{
+ charset: []rune(op.CharSetDigits),
+ charAmount: 9,
+ dashInterval: 3,
+ },
+ reader: rand.Reader,
+ },
+ }
+ for _, tt := range tests {
+ runWithRandReader(tt.reader, func() {
+ b.Run(tt.name, func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _, err := op.NewUserCode(tt.args.charset, tt.args.charAmount, tt.args.dashInterval)
+ require.NoError(b, err)
+ }
+ })
+
+ })
+ }
+}
+
+func TestDeviceAccessToken(t *testing.T) {
+ storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
+ storage.StoreDeviceAuthorization(context.Background(), "native", "qwerty", "yuiop", time.Now().Add(time.Minute), []string{"foo"})
+ storage.CompleteDeviceAuthorization(context.Background(), "yuiop", "tim")
+
+ values := make(url.Values)
+ values.Set("client_id", "native")
+ values.Set("grant_type", string(oidc.GrantTypeDeviceCode))
+ values.Set("device_code", "qwerty")
+
+ r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(values.Encode()))
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w := httptest.NewRecorder()
+
+ op.DeviceAccessToken(w, r, testProvider)
+
+ result := w.Result()
+ got, _ := io.ReadAll(result.Body)
+ t.Log(string(got))
+ assert.Less(t, result.StatusCode, 300)
+ assert.NotEmpty(t, string(got))
+}
+
+func TestCheckDeviceAuthorizationState(t *testing.T) {
+ now := time.Now()
+
+ storage := testProvider.Storage().(op.DeviceAuthorizationStorage)
+ storage.StoreDeviceAuthorization(context.Background(), "native", "pending", "pending", now.Add(time.Minute), []string{"foo"})
+ storage.StoreDeviceAuthorization(context.Background(), "native", "denied", "denied", now.Add(time.Minute), []string{"foo"})
+ storage.StoreDeviceAuthorization(context.Background(), "native", "completed", "completed", now.Add(time.Minute), []string{"foo"})
+ storage.StoreDeviceAuthorization(context.Background(), "native", "expired", "expired", now.Add(-time.Minute), []string{"foo"})
+
+ storage.DenyDeviceAuthorization(context.Background(), "denied")
+ storage.CompleteDeviceAuthorization(context.Background(), "completed", "tim")
+
+ exceededCtx, cancel := context.WithTimeout(context.Background(), -time.Second)
+ defer cancel()
+
+ type args struct {
+ ctx context.Context
+ clientID string
+ deviceCode string
+ }
+ tests := []struct {
+ name string
+ args args
+ want *op.DeviceAuthorizationState
+ wantErr error
+ }{
+ {
+ name: "pending",
+ args: args{
+ ctx: context.Background(),
+ clientID: "native",
+ deviceCode: "pending",
+ },
+ want: &op.DeviceAuthorizationState{
+ ClientID: "native",
+ Scopes: []string{"foo"},
+ Expires: now.Add(time.Minute),
+ },
+ wantErr: oidc.ErrAuthorizationPending(),
+ },
+ {
+ name: "slow down",
+ args: args{
+ ctx: exceededCtx,
+ clientID: "native",
+ deviceCode: "ok",
+ },
+ wantErr: oidc.ErrSlowDown(),
+ },
+ {
+ name: "wrong client",
+ args: args{
+ ctx: context.Background(),
+ clientID: "foo",
+ deviceCode: "ok",
+ },
+ wantErr: oidc.ErrAccessDenied(),
+ },
+ {
+ name: "denied",
+ args: args{
+ ctx: context.Background(),
+ clientID: "native",
+ deviceCode: "denied",
+ },
+ want: &op.DeviceAuthorizationState{
+ ClientID: "native",
+ Scopes: []string{"foo"},
+ Expires: now.Add(time.Minute),
+ Denied: true,
+ },
+ wantErr: oidc.ErrAccessDenied(),
+ },
+ {
+ name: "completed",
+ args: args{
+ ctx: context.Background(),
+ clientID: "native",
+ deviceCode: "completed",
+ },
+ want: &op.DeviceAuthorizationState{
+ ClientID: "native",
+ Scopes: []string{"foo"},
+ Expires: now.Add(time.Minute),
+ Subject: "tim",
+ Done: true,
+ },
+ },
+ {
+ name: "expired",
+ args: args{
+ ctx: context.Background(),
+ clientID: "native",
+ deviceCode: "expired",
+ },
+ want: &op.DeviceAuthorizationState{
+ ClientID: "native",
+ Scopes: []string{"foo"},
+ Expires: now.Add(-time.Minute),
+ },
+ wantErr: oidc.ErrExpiredDeviceCode(),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := op.CheckDeviceAuthorizationState(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, testProvider)
+ require.ErrorIs(t, err, tt.wantErr)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go
index 100bfc8..26f89eb 100644
--- a/pkg/op/discovery.go
+++ b/pkg/op/discovery.go
@@ -1,49 +1,17 @@
package op
import (
+ "context"
"net/http"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ "gopkg.in/square/go-jose.v2"
+
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
-func discoveryHandler(c Configuration, s Signer) func(http.ResponseWriter, *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- Discover(w, CreateDiscoveryConfig(c, s))
- }
-}
-
-func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
- httphelper.MarshalJSON(w, config)
-}
-
-func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration {
- return &oidc.DiscoveryConfiguration{
- Issuer: c.Issuer(),
- AuthorizationEndpoint: c.AuthorizationEndpoint().Absolute(c.Issuer()),
- TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
- IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
- UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
- RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
- EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
- JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
- ScopesSupported: Scopes(c),
- ResponseTypesSupported: ResponseTypes(c),
- GrantTypesSupported: GrantTypes(c),
- SubjectTypesSupported: SubjectTypes(c),
- IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
- RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(c),
- TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
- TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(c),
- IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(c),
- IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
- RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(c),
- RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(c),
- ClaimsSupported: SupportedClaims(c),
- CodeChallengeMethodsSupported: CodeChallengeMethods(c),
- UILocalesSupported: c.SupportedUILocales(),
- RequestParameterSupported: c.RequestObjectSupported(),
- }
+type DiscoverStorage interface {
+ SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
}
var DefaultSupportedScopes = []string{
@@ -55,6 +23,47 @@ var DefaultSupportedScopes = []string{
oidc.ScopeOfflineAccess,
}
+func discoveryHandler(c Configuration, s DiscoverStorage) func(http.ResponseWriter, *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ Discover(w, CreateDiscoveryConfig(r, c, s))
+ }
+}
+
+func Discover(w http.ResponseWriter, config *oidc.DiscoveryConfiguration) {
+ httphelper.MarshalJSON(w, config)
+}
+
+func CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration {
+ issuer := config.IssuerFromRequest(r)
+ return &oidc.DiscoveryConfiguration{
+ Issuer: issuer,
+ AuthorizationEndpoint: config.AuthorizationEndpoint().Absolute(issuer),
+ TokenEndpoint: config.TokenEndpoint().Absolute(issuer),
+ IntrospectionEndpoint: config.IntrospectionEndpoint().Absolute(issuer),
+ UserinfoEndpoint: config.UserinfoEndpoint().Absolute(issuer),
+ RevocationEndpoint: config.RevocationEndpoint().Absolute(issuer),
+ EndSessionEndpoint: config.EndSessionEndpoint().Absolute(issuer),
+ JwksURI: config.KeysEndpoint().Absolute(issuer),
+ DeviceAuthorizationEndpoint: config.DeviceAuthorizationEndpoint().Absolute(issuer),
+ ScopesSupported: Scopes(config),
+ ResponseTypesSupported: ResponseTypes(config),
+ GrantTypesSupported: GrantTypes(config),
+ SubjectTypesSupported: SubjectTypes(config),
+ IDTokenSigningAlgValuesSupported: SigAlgorithms(r.Context(), storage),
+ RequestObjectSigningAlgValuesSupported: RequestObjectSigAlgorithms(config),
+ TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(config),
+ TokenEndpointAuthSigningAlgValuesSupported: TokenSigAlgorithms(config),
+ IntrospectionEndpointAuthSigningAlgValuesSupported: IntrospectionSigAlgorithms(config),
+ IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(config),
+ RevocationEndpointAuthSigningAlgValuesSupported: RevocationSigAlgorithms(config),
+ RevocationEndpointAuthMethodsSupported: AuthMethodsRevocationEndpoint(config),
+ ClaimsSupported: SupportedClaims(config),
+ CodeChallengeMethodsSupported: CodeChallengeMethods(config),
+ UILocalesSupported: config.SupportedUILocales(),
+ RequestParameterSupported: config.RequestObjectSupported(),
+ }
+}
+
func Scopes(c Configuration) []string {
return DefaultSupportedScopes // TODO: config
}
@@ -84,9 +93,94 @@ func GrantTypes(c Configuration) []oidc.GrantType {
if c.GrantTypeJWTAuthorizationSupported() {
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
}
+ if c.GrantTypeDeviceCodeSupported() {
+ grantTypes = append(grantTypes, oidc.GrantTypeDeviceCode)
+ }
return grantTypes
}
+func SubjectTypes(c Configuration) []string {
+ return []string{"public"} //TODO: config
+}
+
+func SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string {
+ algorithms, err := storage.SignatureAlgorithms(ctx)
+ if err != nil {
+ return nil
+ }
+ algs := make([]string, len(algorithms))
+ for i, algorithm := range algorithms {
+ algs[i] = string(algorithm)
+ }
+ return algs
+}
+
+func RequestObjectSigAlgorithms(c Configuration) []string {
+ if !c.RequestObjectSupported() {
+ return nil
+ }
+ return c.RequestObjectSigningAlgorithmsSupported()
+}
+
+func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
+ authMethods := []oidc.AuthMethod{
+ oidc.AuthMethodNone,
+ oidc.AuthMethodBasic,
+ }
+ if c.AuthMethodPostSupported() {
+ authMethods = append(authMethods, oidc.AuthMethodPost)
+ }
+ if c.AuthMethodPrivateKeyJWTSupported() {
+ authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
+ }
+ return authMethods
+}
+
+func TokenSigAlgorithms(c Configuration) []string {
+ if !c.AuthMethodPrivateKeyJWTSupported() {
+ return nil
+ }
+ return c.TokenEndpointSigningAlgorithmsSupported()
+}
+
+func IntrospectionSigAlgorithms(c Configuration) []string {
+ if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
+ return nil
+ }
+ return c.IntrospectionEndpointSigningAlgorithmsSupported()
+}
+
+func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
+ authMethods := []oidc.AuthMethod{
+ oidc.AuthMethodBasic,
+ }
+ if c.AuthMethodPrivateKeyJWTSupported() {
+ authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
+ }
+ return authMethods
+}
+
+func RevocationSigAlgorithms(c Configuration) []string {
+ if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
+ return nil
+ }
+ return c.RevocationEndpointSigningAlgorithmsSupported()
+}
+
+func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
+ authMethods := []oidc.AuthMethod{
+ oidc.AuthMethodNone,
+ oidc.AuthMethodBasic,
+ }
+ if c.AuthMethodPostSupported() {
+ authMethods = append(authMethods, oidc.AuthMethodPost)
+ }
+ if c.AuthMethodPrivateKeyJWTSupported() {
+ authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
+ }
+ return authMethods
+}
+
func SupportedClaims(c Configuration) []string {
return []string{ // TODO: config
"sub",
@@ -116,59 +210,6 @@ func SupportedClaims(c Configuration) []string {
}
}
-func SigAlgorithms(s Signer) []string {
- return []string{string(s.SignatureAlgorithm())}
-}
-
-func SubjectTypes(c Configuration) []string {
- return []string{"public"} // TODO: config
-}
-
-func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
- authMethods := []oidc.AuthMethod{
- oidc.AuthMethodNone,
- oidc.AuthMethodBasic,
- }
- if c.AuthMethodPostSupported() {
- authMethods = append(authMethods, oidc.AuthMethodPost)
- }
- if c.AuthMethodPrivateKeyJWTSupported() {
- authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
- }
- return authMethods
-}
-
-func TokenSigAlgorithms(c Configuration) []string {
- if !c.AuthMethodPrivateKeyJWTSupported() {
- return nil
- }
- return c.TokenEndpointSigningAlgorithmsSupported()
-}
-
-func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
- authMethods := []oidc.AuthMethod{
- oidc.AuthMethodBasic,
- }
- if c.AuthMethodPrivateKeyJWTSupported() {
- authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
- }
- return authMethods
-}
-
-func AuthMethodsRevocationEndpoint(c Configuration) []oidc.AuthMethod {
- authMethods := []oidc.AuthMethod{
- oidc.AuthMethodNone,
- oidc.AuthMethodBasic,
- }
- if c.AuthMethodPostSupported() {
- authMethods = append(authMethods, oidc.AuthMethodPost)
- }
- if c.AuthMethodPrivateKeyJWTSupported() {
- authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
- }
- return authMethods
-}
-
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() {
@@ -176,24 +217,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
}
return codeMethods
}
-
-func IntrospectionSigAlgorithms(c Configuration) []string {
- if !c.IntrospectionAuthMethodPrivateKeyJWTSupported() {
- return nil
- }
- return c.IntrospectionEndpointSigningAlgorithmsSupported()
-}
-
-func RevocationSigAlgorithms(c Configuration) []string {
- if !c.RevocationAuthMethodPrivateKeyJWTSupported() {
- return nil
- }
- return c.RevocationEndpointSigningAlgorithmsSupported()
-}
-
-func RequestObjectSigAlgorithms(c Configuration) []string {
- if !c.RequestObjectSupported() {
- return nil
- }
- return c.RequestObjectSigningAlgorithmsSupported()
-}
diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go
index 1d74f75..2d0b8af 100644
--- a/pkg/op/discovery_test.go
+++ b/pkg/op/discovery_test.go
@@ -1,18 +1,19 @@
package op_test
import (
+ "context"
"net/http"
"net/http/httptest"
- "reflect"
"testing"
"github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
- "github.com/zitadel/oidc/pkg/op/mock"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/op/mock"
)
func TestDiscover(t *testing.T) {
@@ -47,8 +48,9 @@ func TestDiscover(t *testing.T) {
func TestCreateDiscoveryConfig(t *testing.T) {
type args struct {
- c op.Configuration
- s op.Signer
+ request *http.Request
+ c op.Configuration
+ s op.DiscoverStorage
}
tests := []struct {
name string
@@ -59,9 +61,8 @@ func TestCreateDiscoveryConfig(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.CreateDiscoveryConfig(tt.args.c, tt.args.s); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("CreateDiscoveryConfig() = %v, want %v", got, tt.want)
- }
+ got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s)
+ assert.Equal(t, tt.want, got)
})
}
}
@@ -83,9 +84,8 @@ func Test_scopes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.Scopes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("scopes() = %v, want %v", got, tt.want)
- }
+ got := op.Scopes(tt.args.c)
+ assert.Equal(t, tt.want, got)
})
}
}
@@ -99,13 +99,16 @@ func Test_ResponseTypes(t *testing.T) {
args args
want []string
}{
- // TODO: Add test cases.
+ {
+ "code and implicit flow",
+ args{},
+ []string{"code", "id_token", "id_token token"},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.ResponseTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("responseTypes() = %v, want %v", got, tt.want)
- }
+ got := op.ResponseTypes(tt.args.c)
+ assert.Equal(t, tt.want, got)
})
}
}
@@ -117,63 +120,53 @@ func Test_GrantTypes(t *testing.T) {
tests := []struct {
name string
args args
- want []string
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := op.GrantTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("grantTypes() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func TestSupportedClaims(t *testing.T) {
- type args struct {
- c op.Configuration
- }
- tests := []struct {
- name string
- args args
- want []string
- }{
- // TODO: Add test cases.
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := op.SupportedClaims(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("SupportedClaims() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
-func Test_SigAlgorithms(t *testing.T) {
- m := mock.NewMockSigner(gomock.NewController(t))
- type args struct {
- s op.Signer
- }
- tests := []struct {
- name string
- args args
- want []string
+ want []oidc.GrantType
}{
{
- "",
- args{func() op.Signer {
- m.EXPECT().SignatureAlgorithm().Return(jose.RS256)
- return m
- }()},
- []string{"RS256"},
+ "code and implicit flow",
+ args{
+ func() op.Configuration {
+ c := mock.NewMockConfiguration(gomock.NewController(t))
+ c.EXPECT().GrantTypeRefreshTokenSupported().Return(false)
+ c.EXPECT().GrantTypeTokenExchangeSupported().Return(false)
+ c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false)
+ c.EXPECT().GrantTypeClientCredentialsSupported().Return(false)
+ c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
+ return c
+ }(),
+ },
+ []oidc.GrantType{
+ oidc.GrantTypeCode,
+ oidc.GrantTypeImplicit,
+ },
+ },
+ {
+ "code, implicit flow, refresh token, token exchange, jwt profile, client_credentials",
+ args{
+ func() op.Configuration {
+ c := mock.NewMockConfiguration(gomock.NewController(t))
+ c.EXPECT().GrantTypeRefreshTokenSupported().Return(true)
+ c.EXPECT().GrantTypeTokenExchangeSupported().Return(true)
+ c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(true)
+ c.EXPECT().GrantTypeClientCredentialsSupported().Return(true)
+ c.EXPECT().GrantTypeDeviceCodeSupported().Return(false)
+ return c
+ }(),
+ },
+ []oidc.GrantType{
+ oidc.GrantTypeCode,
+ oidc.GrantTypeImplicit,
+ oidc.GrantTypeRefreshToken,
+ oidc.GrantTypeClientCredentials,
+ oidc.GrantTypeTokenExchange,
+ oidc.GrantTypeBearer,
+ },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.SigAlgorithms(tt.args.s); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("sigAlgorithms() = %v, want %v", got, tt.want)
- }
+ got := op.GrantTypes(tt.args.c)
+ assert.Equal(t, tt.want, got)
})
}
}
@@ -195,9 +188,80 @@ func Test_SubjectTypes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.SubjectTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("subjectTypes() = %v, want %v", got, tt.want)
- }
+ got := op.SubjectTypes(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_SigAlgorithms(t *testing.T) {
+ m := mock.NewMockDiscoverStorage(gomock.NewController(t))
+ type args struct {
+ s op.DiscoverStorage
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "",
+ args{func() op.DiscoverStorage {
+ m.EXPECT().SignatureAlgorithms(gomock.Any()).Return([]jose.SignatureAlgorithm{jose.RS256}, nil)
+ return m
+ }()},
+ []string{"RS256"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.SigAlgorithms(context.Background(), tt.args.s)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_RequestObjectSigAlgorithms(t *testing.T) {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "not supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().RequestObjectSupported().Return(false)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().RequestObjectSupported().Return(true)
+ m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return(nil)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, list",
+ args{func() op.Configuration {
+ m.EXPECT().RequestObjectSupported().Return(true)
+ m.EXPECT().RequestObjectSigningAlgorithmsSupported().Return([]string{"RS256"})
+ return m
+ }()},
+ []string{"RS256"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.RequestObjectSigAlgorithms(tt.args.c)
+ assert.Equal(t, tt.want, got)
})
}
}
@@ -244,9 +308,311 @@ func Test_AuthMethodsTokenEndpoint(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("authMethods() = %v, want %v", got, tt.want)
- }
+ got := op.AuthMethodsTokenEndpoint(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_TokenSigAlgorithms(t *testing.T) {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "not supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return(nil)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, list",
+ args{func() op.Configuration {
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().TokenEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
+ return m
+ }()},
+ []string{"RS256"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.TokenSigAlgorithms(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_IntrospectionSigAlgorithms(t *testing.T) {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "not supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return(nil)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, list",
+ args{func() op.Configuration {
+ m.EXPECT().IntrospectionAuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().IntrospectionEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
+ return m
+ }()},
+ []string{"RS256"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.IntrospectionSigAlgorithms(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_AuthMethodsIntrospectionEndpoint(t *testing.T) {
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []oidc.AuthMethod
+ }{
+ {
+ "basic only",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ []oidc.AuthMethod{oidc.AuthMethodBasic},
+ },
+ {
+ "basic and private_key_jwt",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
+ return m
+ }()},
+ []oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.AuthMethodsIntrospectionEndpoint(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_RevocationSigAlgorithms(t *testing.T) {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "not supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, empty",
+ args{func() op.Configuration {
+ m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return(nil)
+ return m
+ }()},
+ nil,
+ },
+ {
+ "supported, list",
+ args{func() op.Configuration {
+ m.EXPECT().RevocationAuthMethodPrivateKeyJWTSupported().Return(true)
+ m.EXPECT().RevocationEndpointSigningAlgorithmsSupported().Return([]string{"RS256"})
+ return m
+ }()},
+ []string{"RS256"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.RevocationSigAlgorithms(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_AuthMethodsRevocationEndpoint(t *testing.T) {
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []oidc.AuthMethod
+ }{
+ {
+ "none and basic",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().AuthMethodPostSupported().Return(false)
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic},
+ },
+ {
+ "none, basic and post",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().AuthMethodPostSupported().Return(true)
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(false)
+ return m
+ }()},
+ []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost},
+ },
+ {
+ "none, basic, post and private_key_jwt",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().AuthMethodPostSupported().Return(true)
+ m.EXPECT().AuthMethodPrivateKeyJWTSupported().Return(true)
+ return m
+ }()},
+ []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.AuthMethodsRevocationEndpoint(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestSupportedClaims(t *testing.T) {
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ "scopes",
+ args{},
+ []string{
+ "sub",
+ "aud",
+ "exp",
+ "iat",
+ "iss",
+ "auth_time",
+ "nonce",
+ "acr",
+ "amr",
+ "c_hash",
+ "at_hash",
+ "act",
+ "scopes",
+ "client_id",
+ "azp",
+ "preferred_username",
+ "name",
+ "family_name",
+ "given_name",
+ "locale",
+ "email",
+ "email_verified",
+ "phone_number",
+ "phone_number_verified",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.SupportedClaims(tt.args.c)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func Test_CodeChallengeMethods(t *testing.T) {
+ type args struct {
+ c op.Configuration
+ }
+ tests := []struct {
+ name string
+ args args
+ want []oidc.CodeChallengeMethod
+ }{
+ {
+ "not supported",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().CodeMethodS256Supported().Return(false)
+ return m
+ }()},
+ []oidc.CodeChallengeMethod{},
+ },
+ {
+ "S256",
+ args{func() op.Configuration {
+ m := mock.NewMockConfiguration(gomock.NewController(t))
+ m.EXPECT().CodeMethodS256Supported().Return(true)
+ return m
+ }()},
+ []oidc.CodeChallengeMethod{oidc.CodeChallengeMethodS256},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := op.CodeChallengeMethods(tt.args.c)
+ assert.Equal(t, tt.want, got)
})
}
}
diff --git a/pkg/op/endpoint_test.go b/pkg/op/endpoint_test.go
index 7c8d1ce..50de89c 100644
--- a/pkg/op/endpoint_test.go
+++ b/pkg/op/endpoint_test.go
@@ -3,7 +3,7 @@ package op_test
import (
"testing"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
func TestEndpoint_Path(t *testing.T) {
diff --git a/pkg/op/error.go b/pkg/op/error.go
index 3c820d6..acca4ab 100644
--- a/pkg/op/error.go
+++ b/pkg/op/error.go
@@ -3,8 +3,8 @@ package op
import (
"net/http"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type ErrAuthRequest interface {
diff --git a/pkg/op/keys.go b/pkg/op/keys.go
index a80211e..239ecbd 100644
--- a/pkg/op/keys.go
+++ b/pkg/op/keys.go
@@ -6,11 +6,11 @@ import (
"gopkg.in/square/go-jose.v2"
- httphelper "github.com/zitadel/oidc/pkg/http"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
)
type KeyProvider interface {
- GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
+ KeySet(context.Context) ([]Key, error)
}
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
@@ -20,10 +20,23 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
}
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
- keySet, err := k.GetKeySet(r.Context())
+ keySet, err := k.KeySet(r.Context())
if err != nil {
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
return
}
- httphelper.MarshalJSON(w, keySet)
+ httphelper.MarshalJSON(w, jsonWebKeySet(keySet))
+}
+
+func jsonWebKeySet(keys []Key) *jose.JSONWebKeySet {
+ webKeys := make([]jose.JSONWebKey, len(keys))
+ for i, key := range keys {
+ webKeys[i] = jose.JSONWebKey{
+ KeyID: key.ID(),
+ Algorithm: string(key.Algorithm()),
+ Use: key.Use(),
+ Key: key.Key(),
+ }
+ }
+ return &jose.JSONWebKeySet{Keys: webKeys}
}
diff --git a/pkg/op/keys_test.go b/pkg/op/keys_test.go
index 7618589..2e56b78 100644
--- a/pkg/op/keys_test.go
+++ b/pkg/op/keys_test.go
@@ -11,9 +11,9 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
- "github.com/zitadel/oidc/pkg/op/mock"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/op/mock"
)
func TestKeys(t *testing.T) {
@@ -35,7 +35,7 @@ func TestKeys(t *testing.T) {
args: args{
k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t))
- m.EXPECT().GetKeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
+ m.EXPECT().KeySet(gomock.Any()).Return(nil, oidc.ErrServerError())
return m
}(),
},
@@ -51,39 +51,39 @@ func TestKeys(t *testing.T) {
args: args{
k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t))
- m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil)
+ m.EXPECT().KeySet(gomock.Any()).Return(nil, nil)
return m
}(),
},
res: res{
statusCode: http.StatusOK,
contentType: "application/json",
+ body: `{"keys":[]}
+`,
},
},
{
name: "list",
args: args{
k: func() op.KeyProvider {
- m := mock.NewMockKeyProvider(gomock.NewController(t))
- m.EXPECT().GetKeySet(gomock.Any()).Return(
- &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{
- {
- Key: &rsa.PublicKey{
- N: big.NewInt(1),
- E: 1,
- },
- KeyID: "id",
- },
- }},
- nil,
- )
+ ctrl := gomock.NewController(t)
+ m := mock.NewMockKeyProvider(ctrl)
+ k := mock.NewMockKey(ctrl)
+ k.EXPECT().Key().Return(&rsa.PublicKey{
+ N: big.NewInt(1),
+ E: 1,
+ })
+ k.EXPECT().ID().Return("id")
+ k.EXPECT().Algorithm().Return(jose.RS256)
+ k.EXPECT().Use().Return("sig")
+ m.EXPECT().KeySet(gomock.Any()).Return([]op.Key{k}, nil)
return m
}(),
},
res: res{
statusCode: http.StatusOK,
contentType: "application/json",
- body: `{"keys":[{"kty":"RSA","kid":"id","n":"AQ","e":"AQ"}]}
+ body: `{"keys":[{"use":"sig","kty":"RSA","kid":"id","alg":"RS256","n":"AQ","e":"AQ"}]}
`,
},
},
diff --git a/pkg/op/mock/authorizer.mock.go b/pkg/op/mock/authorizer.mock.go
index 52f3877..cc913ee 100644
--- a/pkg/op/mock/authorizer.mock.go
+++ b/pkg/op/mock/authorizer.mock.go
@@ -1,15 +1,16 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: Authorizer)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Authorizer)
// Package mock is a generated GoMock package.
package mock
import (
+ context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
- http "github.com/zitadel/oidc/pkg/http"
- op "github.com/zitadel/oidc/pkg/op"
+ http "github.com/zitadel/oidc/v2/pkg/http"
+ op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockAuthorizer is a mock of Authorizer interface.
@@ -78,31 +79,17 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call {
}
// IDTokenHintVerifier mocks base method.
-func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier {
+func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "IDTokenHintVerifier")
+ ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0)
ret0, _ := ret[0].(op.IDTokenHintVerifier)
return ret0
}
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier.
-func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier() *gomock.Call {
+func (mr *MockAuthorizerMockRecorder) IDTokenHintVerifier(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier))
-}
-
-// Issuer mocks base method.
-func (m *MockAuthorizer) 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 *MockAuthorizerMockRecorder) Issuer() *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockAuthorizer)(nil).Issuer))
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0)
}
// RequestObjectSupported mocks base method.
@@ -119,20 +106,6 @@ func (mr *MockAuthorizerMockRecorder) RequestObjectSupported() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestObjectSupported", reflect.TypeOf((*MockAuthorizer)(nil).RequestObjectSupported))
}
-// Signer mocks base method.
-func (m *MockAuthorizer) Signer() op.Signer {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Signer")
- ret0, _ := ret[0].(op.Signer)
- return ret0
-}
-
-// Signer indicates an expected call of Signer.
-func (mr *MockAuthorizerMockRecorder) Signer() *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockAuthorizer)(nil).Signer))
-}
-
// Storage mocks base method.
func (m *MockAuthorizer) Storage() op.Storage {
m.ctrl.T.Helper()
diff --git a/pkg/op/mock/authorizer.mock.impl.go b/pkg/op/mock/authorizer.mock.impl.go
index d4f29d5..3f1d525 100644
--- a/pkg/op/mock/authorizer.mock.impl.go
+++ b/pkg/op/mock/authorizer.mock.impl.go
@@ -8,8 +8,8 @@ import (
"github.com/gorilla/schema"
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
func NewAuthorizer(t *testing.T) op.Authorizer {
@@ -20,23 +20,13 @@ func NewAuthorizerExpectValid(t *testing.T, wantErr bool) op.Authorizer {
m := NewAuthorizer(t)
ExpectDecoder(m)
ExpectEncoder(m)
- ExpectSigner(m, t)
+ //ExpectSigner(m, t)
ExpectStorage(m, t)
ExpectVerifier(m, t)
// ExpectErrorHandler(m, t, wantErr)
return m
}
-// func NewAuthorizerExpectDecoderFails(t *testing.T) op.Authorizer {
-// m := NewAuthorizer(t)
-// ExpectDecoderFails(m)
-// ExpectEncoder(m)
-// ExpectSigner(m, t)
-// ExpectStorage(m, t)
-// ExpectErrorHandler(m, t)
-// return m
-// }
-
func ExpectDecoder(a op.Authorizer) {
mockA := a.(*MockAuthorizer)
mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder())
@@ -47,17 +37,18 @@ func ExpectEncoder(a op.Authorizer) {
mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder())
}
-func ExpectSigner(a op.Authorizer, t *testing.T) {
- mockA := a.(*MockAuthorizer)
- mockA.EXPECT().Signer().DoAndReturn(
- func() op.Signer {
- return &Sig{}
- })
-}
+//
+//func ExpectSigner(a op.Authorizer, t *testing.T) {
+// mockA := a.(*MockAuthorizer)
+// mockA.EXPECT().Signer().DoAndReturn(
+// func() op.Signer {
+// return &Sig{}
+// })
+//}
func ExpectVerifier(a op.Authorizer, t *testing.T) {
mockA := a.(*MockAuthorizer)
- mockA.EXPECT().IDTokenHintVerifier().DoAndReturn(
+ mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn(
func() op.IDTokenHintVerifier {
return op.NewIDTokenHintVerifier("", nil)
})
diff --git a/pkg/op/mock/client.go b/pkg/op/mock/client.go
index 3b16e5e..36df84a 100644
--- a/pkg/op/mock/client.go
+++ b/pkg/op/mock/client.go
@@ -5,8 +5,8 @@ import (
"github.com/golang/mock/gomock"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
func NewClient(t *testing.T) op.Client {
diff --git a/pkg/op/mock/client.mock.go b/pkg/op/mock/client.mock.go
index cfe3703..e3d19fb 100644
--- a/pkg/op/mock/client.mock.go
+++ b/pkg/op/mock/client.mock.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: Client)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Client)
// Package mock is a generated GoMock package.
package mock
@@ -9,8 +9,8 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
- oidc "github.com/zitadel/oidc/pkg/oidc"
- op "github.com/zitadel/oidc/pkg/op"
+ oidc "github.com/zitadel/oidc/v2/pkg/oidc"
+ op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockClient is a mock of Client interface.
diff --git a/pkg/op/mock/configuration.mock.go b/pkg/op/mock/configuration.mock.go
index e0c90dc..fe7d4da 100644
--- a/pkg/op/mock/configuration.mock.go
+++ b/pkg/op/mock/configuration.mock.go
@@ -1,14 +1,15 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: Configuration)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Configuration)
// Package mock is a generated GoMock package.
package mock
import (
+ http "net/http"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
- op "github.com/zitadel/oidc/pkg/op"
+ op "github.com/zitadel/oidc/v2/pkg/op"
language "golang.org/x/text/language"
)
@@ -91,6 +92,34 @@ func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported))
}
+// DeviceAuthorization mocks base method.
+func (m *MockConfiguration) DeviceAuthorization() op.DeviceAuthorizationConfig {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeviceAuthorization")
+ ret0, _ := ret[0].(op.DeviceAuthorizationConfig)
+ return ret0
+}
+
+// DeviceAuthorization indicates an expected call of DeviceAuthorization.
+func (mr *MockConfigurationMockRecorder) DeviceAuthorization() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorization", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorization))
+}
+
+// DeviceAuthorizationEndpoint mocks base method.
+func (m *MockConfiguration) DeviceAuthorizationEndpoint() op.Endpoint {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeviceAuthorizationEndpoint")
+ ret0, _ := ret[0].(op.Endpoint)
+ return ret0
+}
+
+// DeviceAuthorizationEndpoint indicates an expected call of DeviceAuthorizationEndpoint.
+func (mr *MockConfigurationMockRecorder) DeviceAuthorizationEndpoint() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeviceAuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).DeviceAuthorizationEndpoint))
+}
+
// EndSessionEndpoint mocks base method.
func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint {
m.ctrl.T.Helper()
@@ -119,6 +148,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeClientCredentialsSupported() *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeClientCredentialsSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeClientCredentialsSupported))
}
+// GrantTypeDeviceCodeSupported mocks base method.
+func (m *MockConfiguration) GrantTypeDeviceCodeSupported() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GrantTypeDeviceCodeSupported")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// GrantTypeDeviceCodeSupported indicates an expected call of GrantTypeDeviceCodeSupported.
+func (mr *MockConfigurationMockRecorder) GrantTypeDeviceCodeSupported() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeDeviceCodeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeDeviceCodeSupported))
+}
+
// GrantTypeJWTAuthorizationSupported mocks base method.
func (m *MockConfiguration) GrantTypeJWTAuthorizationSupported() bool {
m.ctrl.T.Helper()
@@ -161,6 +204,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported))
}
+// Insecure mocks base method.
+func (m *MockConfiguration) Insecure() bool {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Insecure")
+ ret0, _ := ret[0].(bool)
+ return ret0
+}
+
+// Insecure indicates an expected call of Insecure.
+func (mr *MockConfigurationMockRecorder) Insecure() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insecure", reflect.TypeOf((*MockConfiguration)(nil).Insecure))
+}
+
// IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method.
func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
m.ctrl.T.Helper()
@@ -203,18 +260,18 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsS
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported))
}
-// Issuer mocks base method.
-func (m *MockConfiguration) Issuer() string {
+// IssuerFromRequest mocks base method.
+func (m *MockConfiguration) IssuerFromRequest(arg0 *http.Request) string {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Issuer")
+ ret := m.ctrl.Call(m, "IssuerFromRequest", arg0)
ret0, _ := ret[0].(string)
return ret0
}
-// Issuer indicates an expected call of Issuer.
-func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call {
+// IssuerFromRequest indicates an expected call of IssuerFromRequest.
+func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Issuer", reflect.TypeOf((*MockConfiguration)(nil).Issuer))
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IssuerFromRequest", reflect.TypeOf((*MockConfiguration)(nil).IssuerFromRequest), arg0)
}
// KeysEndpoint mocks base method.
diff --git a/pkg/op/mock/discovery.mock.go b/pkg/op/mock/discovery.mock.go
new file mode 100644
index 0000000..0c78d52
--- /dev/null
+++ b/pkg/op/mock/discovery.mock.go
@@ -0,0 +1,51 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: DiscoverStorage)
+
+// Package mock is a generated GoMock package.
+package mock
+
+import (
+ context "context"
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+ jose "gopkg.in/square/go-jose.v2"
+)
+
+// MockDiscoverStorage is a mock of DiscoverStorage interface.
+type MockDiscoverStorage struct {
+ ctrl *gomock.Controller
+ recorder *MockDiscoverStorageMockRecorder
+}
+
+// MockDiscoverStorageMockRecorder is the mock recorder for MockDiscoverStorage.
+type MockDiscoverStorageMockRecorder struct {
+ mock *MockDiscoverStorage
+}
+
+// NewMockDiscoverStorage creates a new mock instance.
+func NewMockDiscoverStorage(ctrl *gomock.Controller) *MockDiscoverStorage {
+ mock := &MockDiscoverStorage{ctrl: ctrl}
+ mock.recorder = &MockDiscoverStorageMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockDiscoverStorage) EXPECT() *MockDiscoverStorageMockRecorder {
+ return m.recorder
+}
+
+// SignatureAlgorithms mocks base method.
+func (m *MockDiscoverStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
+ ret0, _ := ret[0].([]jose.SignatureAlgorithm)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
+func (mr *MockDiscoverStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockDiscoverStorage)(nil).SignatureAlgorithms), arg0)
+}
diff --git a/pkg/op/mock/generate.go b/pkg/op/mock/generate.go
index c9c7efa..ca288d2 100644
--- a/pkg/op/mock/generate.go
+++ b/pkg/op/mock/generate.go
@@ -1,8 +1,10 @@
package mock
-//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage
-//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/pkg/op Authorizer
-//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/pkg/op Client
-//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/pkg/op Configuration
-//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/pkg/op Signer
-//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider
+//go:generate go install github.com/golang/mock/mockgen@v1.6.0
+//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/v2/pkg/op Storage
+//go:generate mockgen -package mock -destination ./authorizer.mock.go github.com/zitadel/oidc/v2/pkg/op Authorizer
+//go:generate mockgen -package mock -destination ./client.mock.go github.com/zitadel/oidc/v2/pkg/op Client
+//go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/pkg/op Configuration
+//go:generate mockgen -package mock -destination ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage
+//go:generate mockgen -package mock -destination ./signer.mock.go github.com/zitadel/oidc/v2/pkg/op SigningKey,Key
+//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/v2/pkg/op KeyProvider
diff --git a/pkg/op/mock/key.mock.go b/pkg/op/mock/key.mock.go
index 56d12dc..8831651 100644
--- a/pkg/op/mock/key.mock.go
+++ b/pkg/op/mock/key.mock.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: KeyProvider)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: KeyProvider)
// Package mock is a generated GoMock package.
package mock
@@ -9,7 +9,7 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
- jose "gopkg.in/square/go-jose.v2"
+ op "github.com/zitadel/oidc/v2/pkg/op"
)
// MockKeyProvider is a mock of KeyProvider interface.
@@ -35,17 +35,17 @@ func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder {
return m.recorder
}
-// GetKeySet mocks base method.
-func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
+// KeySet mocks base method.
+func (m *MockKeyProvider) KeySet(arg0 context.Context) ([]op.Key, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetKeySet", arg0)
- ret0, _ := ret[0].(*jose.JSONWebKeySet)
+ ret := m.ctrl.Call(m, "KeySet", arg0)
+ ret0, _ := ret[0].([]op.Key)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetKeySet indicates an expected call of GetKeySet.
-func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
+// KeySet indicates an expected call of KeySet.
+func (mr *MockKeyProviderMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockKeyProvider)(nil).GetKeySet), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockKeyProvider)(nil).KeySet), arg0)
}
diff --git a/pkg/op/mock/signer.mock.go b/pkg/op/mock/signer.mock.go
index 42a92fb..78c0efe 100644
--- a/pkg/op/mock/signer.mock.go
+++ b/pkg/op/mock/signer.mock.go
@@ -1,56 +1,69 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: Signer)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: SigningKey,Key)
// Package mock is a generated GoMock package.
package mock
import (
- context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
jose "gopkg.in/square/go-jose.v2"
)
-// MockSigner is a mock of Signer interface.
-type MockSigner struct {
+// MockSigningKey is a mock of SigningKey interface.
+type MockSigningKey struct {
ctrl *gomock.Controller
- recorder *MockSignerMockRecorder
+ recorder *MockSigningKeyMockRecorder
}
-// MockSignerMockRecorder is the mock recorder for MockSigner.
-type MockSignerMockRecorder struct {
- mock *MockSigner
+// MockSigningKeyMockRecorder is the mock recorder for MockSigningKey.
+type MockSigningKeyMockRecorder struct {
+ mock *MockSigningKey
}
-// NewMockSigner creates a new mock instance.
-func NewMockSigner(ctrl *gomock.Controller) *MockSigner {
- mock := &MockSigner{ctrl: ctrl}
- mock.recorder = &MockSignerMockRecorder{mock}
+// NewMockSigningKey creates a new mock instance.
+func NewMockSigningKey(ctrl *gomock.Controller) *MockSigningKey {
+ mock := &MockSigningKey{ctrl: ctrl}
+ mock.recorder = &MockSigningKeyMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockSigner) EXPECT() *MockSignerMockRecorder {
+func (m *MockSigningKey) EXPECT() *MockSigningKeyMockRecorder {
return m.recorder
}
-// Health mocks base method.
-func (m *MockSigner) Health(arg0 context.Context) error {
+// ID mocks base method.
+func (m *MockSigningKey) ID() string {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Health", arg0)
- ret0, _ := ret[0].(error)
+ ret := m.ctrl.Call(m, "ID")
+ ret0, _ := ret[0].(string)
return ret0
}
-// Health indicates an expected call of Health.
-func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call {
+// ID indicates an expected call of ID.
+func (mr *MockSigningKeyMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockSigner)(nil).Health), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockSigningKey)(nil).ID))
+}
+
+// Key mocks base method.
+func (m *MockSigningKey) Key() interface{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Key")
+ ret0, _ := ret[0].(interface{})
+ return ret0
+}
+
+// Key indicates an expected call of Key.
+func (mr *MockSigningKeyMockRecorder) Key() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockSigningKey)(nil).Key))
}
// SignatureAlgorithm mocks base method.
-func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
+func (m *MockSigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignatureAlgorithm")
ret0, _ := ret[0].(jose.SignatureAlgorithm)
@@ -58,21 +71,86 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
}
// SignatureAlgorithm indicates an expected call of SignatureAlgorithm.
-func (mr *MockSignerMockRecorder) SignatureAlgorithm() *gomock.Call {
+func (mr *MockSigningKeyMockRecorder) SignatureAlgorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigner)(nil).SignatureAlgorithm))
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithm", reflect.TypeOf((*MockSigningKey)(nil).SignatureAlgorithm))
}
-// Signer mocks base method.
-func (m *MockSigner) Signer() jose.Signer {
+// MockKey is a mock of Key interface.
+type MockKey struct {
+ ctrl *gomock.Controller
+ recorder *MockKeyMockRecorder
+}
+
+// MockKeyMockRecorder is the mock recorder for MockKey.
+type MockKeyMockRecorder struct {
+ mock *MockKey
+}
+
+// NewMockKey creates a new mock instance.
+func NewMockKey(ctrl *gomock.Controller) *MockKey {
+ mock := &MockKey{ctrl: ctrl}
+ mock.recorder = &MockKeyMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockKey) EXPECT() *MockKeyMockRecorder {
+ return m.recorder
+}
+
+// Algorithm mocks base method.
+func (m *MockKey) Algorithm() jose.SignatureAlgorithm {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Signer")
- ret0, _ := ret[0].(jose.Signer)
+ ret := m.ctrl.Call(m, "Algorithm")
+ ret0, _ := ret[0].(jose.SignatureAlgorithm)
return ret0
}
-// Signer indicates an expected call of Signer.
-func (mr *MockSignerMockRecorder) Signer() *gomock.Call {
+// Algorithm indicates an expected call of Algorithm.
+func (mr *MockKeyMockRecorder) Algorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Signer", reflect.TypeOf((*MockSigner)(nil).Signer))
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockKey)(nil).Algorithm))
+}
+
+// ID mocks base method.
+func (m *MockKey) ID() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ID")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// ID indicates an expected call of ID.
+func (mr *MockKeyMockRecorder) ID() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockKey)(nil).ID))
+}
+
+// Key mocks base method.
+func (m *MockKey) Key() interface{} {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Key")
+ ret0, _ := ret[0].(interface{})
+ return ret0
+}
+
+// Key indicates an expected call of Key.
+func (mr *MockKeyMockRecorder) Key() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockKey)(nil).Key))
+}
+
+// Use mocks base method.
+func (m *MockKey) Use() string {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Use")
+ ret0, _ := ret[0].(string)
+ return ret0
+}
+
+// Use indicates an expected call of Use.
+func (mr *MockKeyMockRecorder) Use() *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockKey)(nil).Use))
}
diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go
index 785a643..85afb2a 100644
--- a/pkg/op/mock/storage.mock.go
+++ b/pkg/op/mock/storage.mock.go
@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/zitadel/oidc/pkg/op (interfaces: Storage)
+// Source: github.com/zitadel/oidc/v2/pkg/op (interfaces: Storage)
// Package mock is a generated GoMock package.
package mock
@@ -10,8 +10,8 @@ import (
time "time"
gomock "github.com/golang/mock/gomock"
- oidc "github.com/zitadel/oidc/pkg/oidc"
- op "github.com/zitadel/oidc/pkg/op"
+ oidc "github.com/zitadel/oidc/v2/pkg/oidc"
+ op "github.com/zitadel/oidc/v2/pkg/op"
jose "gopkg.in/square/go-jose.v2"
)
@@ -159,34 +159,19 @@ func (mr *MockStorageMockRecorder) GetClientByClientID(arg0, arg1 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientByClientID", reflect.TypeOf((*MockStorage)(nil).GetClientByClientID), arg0, arg1)
}
-// GetKeyByIDAndUserID mocks base method.
-func (m *MockStorage) GetKeyByIDAndUserID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
+// GetKeyByIDAndClientID mocks base method.
+func (m *MockStorage) GetKeyByIDAndClientID(arg0 context.Context, arg1, arg2 string) (*jose.JSONWebKey, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetKeyByIDAndUserID", arg0, arg1, arg2)
+ ret := m.ctrl.Call(m, "GetKeyByIDAndClientID", arg0, arg1, arg2)
ret0, _ := ret[0].(*jose.JSONWebKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// GetKeyByIDAndUserID indicates an expected call of GetKeyByIDAndUserID.
-func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interface{}) *gomock.Call {
+// GetKeyByIDAndClientID indicates an expected call of GetKeyByIDAndClientID.
+func (mr *MockStorageMockRecorder) GetKeyByIDAndClientID(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2)
-}
-
-// GetKeySet mocks base method.
-func (m *MockStorage) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetKeySet", arg0)
- ret0, _ := ret[0].(*jose.JSONWebKeySet)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetKeySet indicates an expected call of GetKeySet.
-func (mr *MockStorageMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeySet", reflect.TypeOf((*MockStorage)(nil).GetKeySet), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndClientID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndClientID), arg0, arg1, arg2)
}
// GetPrivateClaimsFromScopes mocks base method.
@@ -204,16 +189,20 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3)
}
-// GetSigningKey mocks base method.
-func (m *MockStorage) GetSigningKey(arg0 context.Context, arg1 chan<- jose.SigningKey) {
+// GetRefreshTokenInfo mocks base method.
+func (m *MockStorage) GetRefreshTokenInfo(arg0 context.Context, arg1, arg2 string) (string, string, error) {
m.ctrl.T.Helper()
- m.ctrl.Call(m, "GetSigningKey", arg0, arg1)
+ ret := m.ctrl.Call(m, "GetRefreshTokenInfo", arg0, arg1, arg2)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(string)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
}
-// GetSigningKey indicates an expected call of GetSigningKey.
-func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call {
+// GetRefreshTokenInfo indicates an expected call of GetRefreshTokenInfo.
+func (mr *MockStorageMockRecorder) GetRefreshTokenInfo(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRefreshTokenInfo", reflect.TypeOf((*MockStorage)(nil).GetRefreshTokenInfo), arg0, arg1, arg2)
}
// Health mocks base method.
@@ -230,6 +219,21 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0)
}
+// KeySet mocks base method.
+func (m *MockStorage) KeySet(arg0 context.Context) ([]op.Key, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "KeySet", arg0)
+ ret0, _ := ret[0].([]op.Key)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// KeySet indicates an expected call of KeySet.
+func (mr *MockStorageMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockStorage)(nil).KeySet), arg0)
+}
+
// RevokeToken mocks base method.
func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error {
m.ctrl.T.Helper()
@@ -259,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)
@@ -273,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)
@@ -287,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)
@@ -300,6 +304,36 @@ func (mr *MockStorageMockRecorder) SetUserinfoFromToken(arg0, arg1, arg2, arg3,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).SetUserinfoFromToken), arg0, arg1, arg2, arg3, arg4)
}
+// SignatureAlgorithms mocks base method.
+func (m *MockStorage) SignatureAlgorithms(arg0 context.Context) ([]jose.SignatureAlgorithm, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SignatureAlgorithms", arg0)
+ ret0, _ := ret[0].([]jose.SignatureAlgorithm)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SignatureAlgorithms indicates an expected call of SignatureAlgorithms.
+func (mr *MockStorageMockRecorder) SignatureAlgorithms(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignatureAlgorithms", reflect.TypeOf((*MockStorage)(nil).SignatureAlgorithms), arg0)
+}
+
+// SigningKey mocks base method.
+func (m *MockStorage) SigningKey(arg0 context.Context) (op.SigningKey, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SigningKey", arg0)
+ ret0, _ := ret[0].(op.SigningKey)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SigningKey indicates an expected call of SigningKey.
+func (mr *MockStorageMockRecorder) SigningKey(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SigningKey", reflect.TypeOf((*MockStorage)(nil).SigningKey), arg0)
+}
+
// TerminateSession mocks base method.
func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go
index 946cee0..9269f89 100644
--- a/pkg/op/mock/storage.mock.impl.go
+++ b/pkg/op/mock/storage.mock.impl.go
@@ -6,13 +6,10 @@ import (
"testing"
"time"
- "github.com/zitadel/oidc/pkg/oidc"
-
- "gopkg.in/square/go-jose.v2"
-
"github.com/golang/mock/gomock"
- "github.com/zitadel/oidc/pkg/op"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
)
func NewStorage(t *testing.T) op.Storage {
@@ -41,13 +38,13 @@ func NewMockStorageAny(t *testing.T) op.Storage {
func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage {
m := NewStorage(t)
- ExpectSigningKeyInvalid(m)
+ //ExpectSigningKeyInvalid(m)
return m
}
func NewMockStorageSigningKey(t *testing.T) op.Storage {
m := NewStorage(t)
- ExpectSigningKey(m)
+ //ExpectSigningKey(m)
return m
}
@@ -85,24 +82,6 @@ func ExpectValidClientID(s op.Storage) {
})
}
-func ExpectSigningKeyInvalid(s op.Storage) {
- mockS := s.(*MockStorage)
- mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
- func(_ context.Context, keyCh chan<- jose.SigningKey) {
- keyCh <- jose.SigningKey{}
- },
- )
-}
-
-func ExpectSigningKey(s op.Storage) {
- mockS := s.(*MockStorage)
- mockS.EXPECT().GetSigningKey(gomock.Any(), gomock.Any()).DoAndReturn(
- func(_ context.Context, keyCh chan<- jose.SigningKey) {
- keyCh <- jose.SigningKey{Algorithm: jose.HS256, Key: []byte("key")}
- },
- )
-}
-
type ConfClient struct {
id string
appType op.ApplicationType
diff --git a/pkg/op/op.go b/pkg/op/op.go
index d85dcd6..ecb753e 100644
--- a/pkg/op/op.go
+++ b/pkg/op/op.go
@@ -12,8 +12,8 @@ import (
"golang.org/x/text/language"
"gopkg.in/square/go-jose.v2"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
const (
@@ -27,80 +27,84 @@ const (
defaultRevocationEndpoint = "revoke"
defaultEndSessionEndpoint = "end_session"
defaultKeysEndpoint = "keys"
+ defaultDeviceAuthzEndpoint = "/device_authorization"
)
-var DefaultEndpoints = &endpoints{
- Authorization: NewEndpoint(defaultAuthorizationEndpoint),
- Token: NewEndpoint(defaultTokenEndpoint),
- Introspection: NewEndpoint(defaultIntrospectEndpoint),
- Userinfo: NewEndpoint(defaultUserinfoEndpoint),
- Revocation: NewEndpoint(defaultRevocationEndpoint),
- EndSession: NewEndpoint(defaultEndSessionEndpoint),
- JwksURI: NewEndpoint(defaultKeysEndpoint),
-}
+var (
+ DefaultEndpoints = &endpoints{
+ Authorization: NewEndpoint(defaultAuthorizationEndpoint),
+ Token: NewEndpoint(defaultTokenEndpoint),
+ Introspection: NewEndpoint(defaultIntrospectEndpoint),
+ Userinfo: NewEndpoint(defaultUserinfoEndpoint),
+ Revocation: NewEndpoint(defaultRevocationEndpoint),
+ EndSession: NewEndpoint(defaultEndSessionEndpoint),
+ JwksURI: NewEndpoint(defaultKeysEndpoint),
+ DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint),
+ }
+
+ defaultCORSOptions = cors.Options{
+ AllowCredentials: true,
+ AllowedHeaders: []string{
+ "Origin",
+ "Accept",
+ "Accept-Language",
+ "Authorization",
+ "Content-Type",
+ "X-Requested-With",
+ },
+ AllowedMethods: []string{
+ http.MethodGet,
+ http.MethodHead,
+ http.MethodPost,
+ },
+ ExposedHeaders: []string{
+ "Location",
+ "Content-Length",
+ },
+ AllowOriginFunc: func(_ string) bool {
+ return true
+ },
+ }
+)
type OpenIDProvider interface {
Configuration
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
- IDTokenHintVerifier() IDTokenHintVerifier
- AccessTokenVerifier() AccessTokenVerifier
+ IDTokenHintVerifier(context.Context) IDTokenHintVerifier
+ AccessTokenVerifier(context.Context) AccessTokenVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
- Signer() Signer
Probes() []ProbesFn
HttpHandler() http.Handler
}
type HttpInterceptor func(http.Handler) http.Handler
-var defaultCORSOptions = cors.Options{
- AllowCredentials: true,
- AllowedHeaders: []string{
- "Origin",
- "Accept",
- "Accept-Language",
- "Authorization",
- "Content-Type",
- "X-Requested-With",
- },
- AllowedMethods: []string{
- http.MethodGet,
- http.MethodHead,
- http.MethodPost,
- },
- ExposedHeaders: []string{
- "Location",
- "Content-Length",
- },
- AllowOriginFunc: func(_ string) bool {
- return true
- },
-}
-
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router {
- intercept := buildInterceptor(interceptors...)
router := mux.NewRouter()
router.Use(cors.New(defaultCORSOptions).Handler)
+ router.Use(intercept(o.IssuerFromRequest, interceptors...))
router.HandleFunc(healthEndpoint, healthHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
- router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer()))
- router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o)))
- router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o)))
- router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o)))
+ router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage()))
+ router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o))
+ router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").HandlerFunc(authorizeCallbackHandler(o))
+ router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o))
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o))
- router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
+ router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o))
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
+ router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), DeviceAuthorizationHandler(o))
return router
}
// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login
-func AuthCallbackURL(o OpenIDProvider) func(string) string {
- return func(requestID string) string {
- return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID
+func AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string {
+ return func(ctx context.Context, requestID string) string {
+ return o.AuthorizationEndpoint().Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID
}
}
@@ -109,7 +113,6 @@ func authCallbackPath(o OpenIDProvider) string {
}
type Config struct {
- Issuer string
CryptoKey [32]byte
DefaultLogoutRedirectURI string
CodeMethodS256 bool
@@ -118,44 +121,52 @@ type Config struct {
GrantTypeRefreshToken bool
RequestObjectSupported bool
SupportedUILocales []language.Tag
+ DeviceAuthorization DeviceAuthorizationConfig
}
type endpoints struct {
- Authorization Endpoint
- Token Endpoint
- Introspection Endpoint
- Userinfo Endpoint
- Revocation Endpoint
- EndSession Endpoint
- CheckSessionIframe Endpoint
- JwksURI Endpoint
+ Authorization Endpoint
+ Token Endpoint
+ Introspection Endpoint
+ Userinfo Endpoint
+ Revocation Endpoint
+ EndSession Endpoint
+ CheckSessionIframe Endpoint
+ JwksURI Endpoint
+ DeviceAuthorization Endpoint
}
// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler())
// a http.Router that handles a suite of endpoints (some paths can be overridden):
-// /healthz
-// /ready
-// /.well-known/openid-configuration
-// /oauth/token
-// /oauth/introspect
-// /callback
-// /authorize
-// /userinfo
-// /revoke
-// /end_session
-// /keys
+//
+// /healthz
+// /ready
+// /.well-known/openid-configuration
+// /oauth/token
+// /oauth/introspect
+// /callback
+// /authorize
+// /userinfo
+// /revoke
+// /end_session
+// /keys
+// /device_authorization
+//
// This does not include login. Login is handled with a redirect that includes the
// request ID. The redirect for logins is specified per-client by Client.LoginURL().
// Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter.
-func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error) {
- err := ValidateIssuer(config.Issuer)
- if err != nil {
- return nil, err
- }
+func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
+ return newProvider(config, storage, StaticIssuer(issuer), opOpts...)
+}
- o := &openidProvider{
+func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
+ return newProvider(config, storage, IssuerFromHost(path), opOpts...)
+}
+
+func newProvider(config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) {
+ o := &Provider{
config: config,
storage: storage,
endpoints: DefaultEndpoints,
@@ -168,36 +179,32 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO
}
}
- keyCh := make(chan jose.SigningKey)
- go storage.GetSigningKey(ctx, keyCh)
- o.signer = NewSigner(ctx, storage, keyCh)
+ o.issuer, err = issuer(o.insecure)
+ if err != nil {
+ return nil, err
+ }
o.httpHandler = CreateRouter(o, o.interceptors...)
o.decoder = schema.NewDecoder()
o.decoder.IgnoreUnknownKeys(true)
- o.encoder = schema.NewEncoder()
+ o.encoder = oidc.NewEncoder()
o.crypto = NewAESCrypto(config.CryptoKey)
// Avoid potential race conditions by calling these early
- _ = o.AccessTokenVerifier() // sets accessTokenVerifier
- _ = o.IDTokenHintVerifier() // sets idTokenHintVerifier
- _ = o.JWTProfileVerifier() // sets jwtProfileVerifier
- _ = o.openIDKeySet() // sets keySet
+ _ = o.openIDKeySet() // sets keySet
return o, nil
}
-type openidProvider struct {
+type Provider struct {
config *Config
+ issuer IssuerFromRequest
+ insecure bool
endpoints *endpoints
storage Storage
- signer Signer
- idTokenHintVerifier IDTokenHintVerifier
- jwtProfileVerifier JWTProfileVerifier
- accessTokenVerifier AccessTokenVerifier
keySet *openIDKeySet
crypto Crypto
httpHandler http.Handler
@@ -209,159 +216,163 @@ type openidProvider struct {
idTokenHintVerifierOpts []IDTokenHintVerifierOpt
}
-func (o *openidProvider) Issuer() string {
- return o.config.Issuer
+func (o *Provider) IssuerFromRequest(r *http.Request) string {
+ return o.issuer(r)
}
-func (o *openidProvider) AuthorizationEndpoint() Endpoint {
+func (o *Provider) Insecure() bool {
+ return o.insecure
+}
+
+func (o *Provider) AuthorizationEndpoint() Endpoint {
return o.endpoints.Authorization
}
-func (o *openidProvider) TokenEndpoint() Endpoint {
+func (o *Provider) TokenEndpoint() Endpoint {
return o.endpoints.Token
}
-func (o *openidProvider) IntrospectionEndpoint() Endpoint {
+func (o *Provider) IntrospectionEndpoint() Endpoint {
return o.endpoints.Introspection
}
-func (o *openidProvider) UserinfoEndpoint() Endpoint {
+func (o *Provider) UserinfoEndpoint() Endpoint {
return o.endpoints.Userinfo
}
-func (o *openidProvider) RevocationEndpoint() Endpoint {
+func (o *Provider) RevocationEndpoint() Endpoint {
return o.endpoints.Revocation
}
-func (o *openidProvider) EndSessionEndpoint() Endpoint {
+func (o *Provider) EndSessionEndpoint() Endpoint {
return o.endpoints.EndSession
}
-func (o *openidProvider) KeysEndpoint() Endpoint {
+func (o *Provider) DeviceAuthorizationEndpoint() Endpoint {
+ return o.endpoints.DeviceAuthorization
+}
+
+func (o *Provider) KeysEndpoint() Endpoint {
return o.endpoints.JwksURI
}
-func (o *openidProvider) AuthMethodPostSupported() bool {
+func (o *Provider) AuthMethodPostSupported() bool {
return o.config.AuthMethodPost
}
-func (o *openidProvider) CodeMethodS256Supported() bool {
+func (o *Provider) CodeMethodS256Supported() bool {
return o.config.CodeMethodS256
}
-func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool {
+func (o *Provider) AuthMethodPrivateKeyJWTSupported() bool {
return o.config.AuthMethodPrivateKeyJWT
}
-func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string {
+func (o *Provider) TokenEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
-func (o *openidProvider) GrantTypeRefreshTokenSupported() bool {
+func (o *Provider) GrantTypeRefreshTokenSupported() bool {
return o.config.GrantTypeRefreshToken
}
-func (o *openidProvider) GrantTypeTokenExchangeSupported() bool {
- return false
+func (o *Provider) GrantTypeTokenExchangeSupported() bool {
+ _, ok := o.storage.(TokenExchangeStorage)
+ return ok
}
-func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool {
+func (o *Provider) GrantTypeJWTAuthorizationSupported() bool {
return true
}
-func (o *openidProvider) GrantTypeClientCredentialsSupported() bool {
+func (o *Provider) GrantTypeDeviceCodeSupported() bool {
+ _, ok := o.storage.(DeviceAuthorizationStorage)
+ return ok
+}
+
+func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
+ return true
+}
+
+func (o *Provider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
+ return []string{"RS256"}
+}
+
+func (o *Provider) GrantTypeClientCredentialsSupported() bool {
_, ok := o.storage.(ClientCredentialsStorage)
return ok
}
-func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
+func (o *Provider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
return true
}
-func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
+func (o *Provider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
-func (o *openidProvider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
- return true
-}
-
-func (o *openidProvider) RevocationEndpointSigningAlgorithmsSupported() []string {
- return []string{"RS256"}
-}
-
-func (o *openidProvider) RequestObjectSupported() bool {
+func (o *Provider) RequestObjectSupported() bool {
return o.config.RequestObjectSupported
}
-func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string {
+func (o *Provider) RequestObjectSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
-func (o *openidProvider) SupportedUILocales() []language.Tag {
+func (o *Provider) SupportedUILocales() []language.Tag {
return o.config.SupportedUILocales
}
-func (o *openidProvider) Storage() Storage {
+func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
+ return o.config.DeviceAuthorization
+}
+
+func (o *Provider) Storage() Storage {
return o.storage
}
-func (o *openidProvider) Decoder() httphelper.Decoder {
+func (o *Provider) Decoder() httphelper.Decoder {
return o.decoder
}
-func (o *openidProvider) Encoder() httphelper.Encoder {
+func (o *Provider) Encoder() httphelper.Encoder {
return o.encoder
}
-func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier {
- if o.idTokenHintVerifier == nil {
- o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
- }
- return o.idTokenHintVerifier
+func (o *Provider) IDTokenHintVerifier(ctx context.Context) IDTokenHintVerifier {
+ return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
}
-func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier {
- if o.jwtProfileVerifier == nil {
- o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer(), 1*time.Hour, time.Second)
- }
- return o.jwtProfileVerifier
+func (o *Provider) JWTProfileVerifier(ctx context.Context) JWTProfileVerifier {
+ return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second)
}
-func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier {
- if o.accessTokenVerifier == nil {
- o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet(), o.accessTokenVerifierOpts...)
- }
- return o.accessTokenVerifier
+func (o *Provider) AccessTokenVerifier(ctx context.Context) AccessTokenVerifier {
+ return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...)
}
-func (o *openidProvider) openIDKeySet() oidc.KeySet {
+func (o *Provider) openIDKeySet() oidc.KeySet {
if o.keySet == nil {
o.keySet = &openIDKeySet{o.Storage()}
}
return o.keySet
}
-func (o *openidProvider) Crypto() Crypto {
+func (o *Provider) Crypto() Crypto {
return o.crypto
}
-func (o *openidProvider) DefaultLogoutRedirectURI() string {
+func (o *Provider) DefaultLogoutRedirectURI() string {
return o.config.DefaultLogoutRedirectURI
}
-func (o *openidProvider) Signer() Signer {
- return o.signer
-}
-
-func (o *openidProvider) Probes() []ProbesFn {
+func (o *Provider) Probes() []ProbesFn {
return []ProbesFn{
- ReadySigner(o.Signer()),
ReadyStorage(o.Storage()),
}
}
-func (o *openidProvider) HttpHandler() http.Handler {
+func (o *Provider) HttpHandler() http.Handler {
return o.httpHandler
}
@@ -372,22 +383,31 @@ type openIDKeySet struct {
// VerifySignature implements the oidc.KeySet interface
// providing an implementation for the keys stored in the OP Storage interface
func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
- keySet, err := o.Storage.GetKeySet(ctx)
+ keySet, err := o.Storage.KeySet(ctx)
if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err)
}
keyID, alg := oidc.GetKeyIDAndAlg(jws)
- key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...)
+ key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, jsonWebKeySet(keySet).Keys...)
if err != nil {
return nil, fmt.Errorf("invalid signature: %w", err)
}
return jws.Verify(&key)
}
-type Option func(o *openidProvider) error
+type Option func(o *Provider) error
+
+// WithAllowInsecure allows the use of http (instead of https) for issuers
+// this is not recommended for production use and violates the OIDC specification
+func WithAllowInsecure() Option {
+ return func(o *Provider) error {
+ o.insecure = true
+ return nil
+ }
+}
func WithCustomAuthEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -397,7 +417,7 @@ func WithCustomAuthEndpoint(endpoint Endpoint) Option {
}
func WithCustomTokenEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -407,7 +427,7 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option {
}
func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -417,7 +437,7 @@ func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
}
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -427,7 +447,7 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
}
func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -437,7 +457,7 @@ func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
}
func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -447,7 +467,7 @@ func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
}
func WithCustomKeysEndpoint(endpoint Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
@@ -457,7 +477,7 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option {
}
func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys Endpoint) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
o.endpoints.Authorization = auth
o.endpoints.Token = token
o.endpoints.Userinfo = userInfo
@@ -469,38 +489,32 @@ func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys End
}
func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
o.interceptors = append(o.interceptors, interceptors...)
return nil
}
}
func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
o.accessTokenVerifierOpts = opts
return nil
}
}
func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
- return func(o *openidProvider) error {
+ return func(o *Provider) error {
o.idTokenHintVerifierOpts = opts
return nil
}
}
-func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
- return func(handlerFunc http.HandlerFunc) http.Handler {
- handler := handlerFuncToHandler(handlerFunc)
+func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler {
+ issuerInterceptor := NewIssuerInterceptor(i)
+ return func(handler http.Handler) http.Handler {
for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler)
}
- return handler
+ return cors.New(defaultCORSOptions).Handler(issuerInterceptor.Handler(handler))
}
}
-
-func handlerFuncToHandler(handlerFunc http.HandlerFunc) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handlerFunc(w, r)
- })
-}
diff --git a/pkg/op/op_test.go b/pkg/op/op_test.go
new file mode 100644
index 0000000..ba3570b
--- /dev/null
+++ b/pkg/op/op_test.go
@@ -0,0 +1,392 @@
+package op_test
+
+import (
+ "context"
+ "crypto/sha256"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/muhlemmer/gu"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/zitadel/oidc/v2/example/server/storage"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/op"
+ "golang.org/x/text/language"
+)
+
+var testProvider op.OpenIDProvider
+
+const (
+ testIssuer = "https://localhost:9998/"
+ pathLoggedOut = "/logged-out"
+)
+
+func init() {
+ config := &op.Config{
+ CryptoKey: sha256.Sum256([]byte("test")),
+ DefaultLogoutRedirectURI: pathLoggedOut,
+ CodeMethodS256: true,
+ AuthMethodPost: true,
+ AuthMethodPrivateKeyJWT: true,
+ GrantTypeRefreshToken: true,
+ RequestObjectSupported: true,
+ SupportedUILocales: []language.Tag{language.English},
+ DeviceAuthorization: op.DeviceAuthorizationConfig{
+ Lifetime: 5 * time.Minute,
+ PollInterval: 5 * time.Second,
+ UserFormURL: testIssuer + "device",
+ UserCode: op.UserCodeBase20,
+ },
+ }
+
+ storage.RegisterClients(
+ storage.NativeClient("native"),
+ storage.WebClient("web", "secret", "https://example.com"),
+ storage.WebClient("api", "secret"),
+ )
+
+ var err error
+ testProvider, err = op.NewOpenIDProvider(testIssuer, config,
+ storage.NewStorage(storage.NewUserStore(testIssuer)), op.WithAllowInsecure(),
+ )
+ if err != nil {
+ panic(err)
+ }
+}
+
+type routesTestStorage interface {
+ op.Storage
+ AuthRequestDone(id string) error
+}
+
+func mapAsValues(m map[string]string) string {
+ values := make(url.Values, len(m))
+ for k, v := range m {
+ values.Set(k, v)
+ }
+ return values.Encode()
+}
+
+func TestRoutes(t *testing.T) {
+ storage := testProvider.Storage().(routesTestStorage)
+ ctx := op.ContextWithIssuer(context.Background(), testIssuer)
+
+ client, err := storage.GetClientByClientID(ctx, "web")
+ require.NoError(t, err)
+
+ oidcAuthReq := &oidc.AuthRequest{
+ ClientID: client.GetID(),
+ RedirectURI: "https://example.com",
+ MaxAge: gu.Ptr[uint](300),
+ Scopes: oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopePhone},
+ ResponseType: oidc.ResponseTypeCode,
+ }
+
+ authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1")
+ require.NoError(t, err)
+ storage.AuthRequestDone(authReq.GetID())
+
+ accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "")
+ require.NoError(t, err)
+ accessTokenRevoke, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "")
+ require.NoError(t, err)
+ idToken, err := op.CreateIDToken(ctx, testIssuer, authReq, time.Hour, accessToken, "123", storage, client)
+ require.NoError(t, err)
+ jwtToken, _, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeJWT, testProvider, client, "")
+ require.NoError(t, err)
+
+ oidcAuthReq.IDTokenHint = idToken
+
+ serverURL, err := url.Parse(testIssuer)
+ require.NoError(t, err)
+
+ type basicAuth struct {
+ username, password string
+ }
+
+ tests := []struct {
+ name string
+ method string
+ path string
+ basicAuth *basicAuth
+ header map[string]string
+ values map[string]string
+ body map[string]string
+ wantCode int
+ headerContains map[string]string
+ json string // test for exact json output
+ contains []string // when the body output is not constant, we just check for snippets to be present in the response
+ }{
+ {
+ name: "health",
+ method: http.MethodGet,
+ path: "/healthz",
+ wantCode: http.StatusOK,
+ json: `{"status":"ok"}`,
+ },
+ {
+ name: "ready",
+ method: http.MethodGet,
+ path: "/ready",
+ wantCode: http.StatusOK,
+ json: `{"status":"ok"}`,
+ },
+ {
+ name: "discovery",
+ method: http.MethodGet,
+ path: oidc.DiscoveryEndpoint,
+ wantCode: http.StatusOK,
+ json: `{"issuer":"https://localhost:9998/","authorization_endpoint":"https://localhost:9998/authorize","token_endpoint":"https://localhost:9998/oauth/token","introspection_endpoint":"https://localhost:9998/oauth/introspect","userinfo_endpoint":"https://localhost:9998/userinfo","revocation_endpoint":"https://localhost:9998/revoke","end_session_endpoint":"https://localhost:9998/end_session","device_authorization_endpoint":"https://localhost:9998/device_authorization","jwks_uri":"https://localhost:9998/keys","scopes_supported":["openid","profile","email","phone","address","offline_access"],"response_types_supported":["code","id_token","id_token token"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:jwt-bearer","urn:ietf:params:oauth:grant-type:device_code"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"request_object_signing_alg_values_supported":["RS256"],"token_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"revocation_endpoint_auth_methods_supported":["none","client_secret_basic","client_secret_post","private_key_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["RS256"],"introspection_endpoint_auth_methods_supported":["client_secret_basic","private_key_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["RS256"],"claims_supported":["sub","aud","exp","iat","iss","auth_time","nonce","acr","amr","c_hash","at_hash","act","scopes","client_id","azp","preferred_username","name","family_name","given_name","locale","email","email_verified","phone_number","phone_number_verified"],"code_challenge_methods_supported":["S256"],"ui_locales_supported":["en"],"request_parameter_supported":true,"request_uri_parameter_supported":false}`,
+ },
+ {
+ name: "authorization",
+ method: http.MethodGet,
+ path: testProvider.AuthorizationEndpoint().Relative(),
+ values: map[string]string{
+ "client_id": client.GetID(),
+ "redirect_uri": "https://example.com",
+ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
+ "response_type": string(oidc.ResponseTypeCode),
+ },
+ wantCode: http.StatusFound,
+ headerContains: map[string]string{"Location": "/login/username?authRequestID="},
+ },
+ {
+ name: "authorization callback",
+ method: http.MethodGet,
+ path: testProvider.AuthorizationEndpoint().Relative() + "/callback",
+ values: map[string]string{"id": authReq.GetID()},
+ wantCode: http.StatusFound,
+ headerContains: map[string]string{"Location": "https://example.com?code="},
+ contains: []string{
+ `Found.",
+ },
+ },
+ {
+ // This call will fail. A successfull test is already
+ // part of client/integration_test.go
+ name: "code exchange",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ values: map[string]string{
+ "grant_type": string(oidc.GrantTypeCode),
+ "code": "123",
+ },
+ wantCode: http.StatusUnauthorized,
+ json: `{"error":"invalid_client"}`,
+ },
+ {
+ name: "JWT authorization",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ values: map[string]string{
+ "grant_type": string(oidc.GrantTypeBearer),
+ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
+ "assertion": jwtToken,
+ },
+ wantCode: http.StatusBadRequest,
+ json: "{\"error\":\"server_error\",\"error_description\":\"audience is not valid: Audience must contain client_id \\\"https://localhost:9998/\\\"\"}",
+ },
+ {
+ name: "Token exchange",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ basicAuth: &basicAuth{"web", "secret"},
+ values: map[string]string{
+ "grant_type": string(oidc.GrantTypeTokenExchange),
+ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
+ "subject_token": jwtToken,
+ "subject_token_type": string(oidc.AccessTokenType),
+ },
+ wantCode: http.StatusOK,
+ contains: []string{
+ `{"access_token":"`,
+ `","issued_token_type":"urn:ietf:params:oauth:token-type:refresh_token","token_type":"Bearer","expires_in":299,"scope":"openid offline_access","refresh_token":"`,
+ },
+ },
+ {
+ name: "Client credentials exchange",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ basicAuth: &basicAuth{"sid1", "verysecret"},
+ values: map[string]string{
+ "grant_type": string(oidc.GrantTypeClientCredentials),
+ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
+ },
+ wantCode: http.StatusOK,
+ contains: []string{`{"access_token":"`, `","token_type":"Bearer","expires_in":299}`},
+ },
+ {
+ // This call will fail. A successfull test is already
+ // part of device_test.go
+ name: "device token",
+ method: http.MethodPost,
+ path: testProvider.TokenEndpoint().Relative(),
+ basicAuth: &basicAuth{"web", "secret"},
+ header: map[string]string{
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: map[string]string{
+ "grant_type": string(oidc.GrantTypeDeviceCode),
+ "device_code": "123",
+ },
+ wantCode: http.StatusBadRequest,
+ json: `{"error":"access_denied","error_description":"The authorization request was denied."}`,
+ },
+ {
+ name: "missing grant type",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ wantCode: http.StatusBadRequest,
+ json: `{"error":"invalid_request","error_description":"grant_type missing"}`,
+ },
+ {
+ name: "unsupported grant type",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ values: map[string]string{
+ "grant_type": "foo",
+ },
+ wantCode: http.StatusBadRequest,
+ json: `{"error":"unsupported_grant_type","error_description":"foo not supported"}`,
+ },
+ {
+ name: "introspection",
+ method: http.MethodGet,
+ path: testProvider.IntrospectionEndpoint().Relative(),
+ basicAuth: &basicAuth{"web", "secret"},
+ values: map[string]string{
+ "token": accessToken,
+ },
+ wantCode: http.StatusOK,
+ json: `{"active":true,"scope":"openid offline_access email profile phone","client_id":"web","sub":"id1","username":"test-user@localhost","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`,
+ },
+ {
+ name: "user info",
+ method: http.MethodGet,
+ path: testProvider.UserinfoEndpoint().Relative(),
+ header: map[string]string{
+ "authorization": "Bearer " + accessToken,
+ },
+ wantCode: http.StatusOK,
+ json: `{"sub":"id1","name":"Test User","given_name":"Test","family_name":"User","locale":"de","preferred_username":"test-user@localhost","email":"test-user@zitadel.ch","email_verified":true}`,
+ },
+ {
+ name: "refresh token",
+ method: http.MethodGet,
+ path: testProvider.TokenEndpoint().Relative(),
+ values: map[string]string{
+ "grant_type": string(oidc.GrantTypeRefreshToken),
+ "refresh_token": refreshToken,
+ "client_id": client.GetID(),
+ "client_secret": "secret",
+ },
+ wantCode: http.StatusOK,
+ contains: []string{
+ `{"access_token":"`,
+ `","token_type":"Bearer","refresh_token":"`,
+ `","expires_in":299,"id_token":"`,
+ },
+ },
+ {
+ name: "revoke",
+ method: http.MethodGet,
+ path: testProvider.RevocationEndpoint().Relative(),
+ basicAuth: &basicAuth{"web", "secret"},
+ values: map[string]string{
+ "token": accessTokenRevoke,
+ },
+ wantCode: http.StatusOK,
+ },
+ {
+ name: "end session",
+ method: http.MethodGet,
+ path: testProvider.EndSessionEndpoint().Relative(),
+ values: map[string]string{
+ "id_token_hint": idToken,
+ "client_id": "web",
+ },
+ wantCode: http.StatusFound,
+ headerContains: map[string]string{"Location": "/logged-out"},
+ contains: []string{`Found.`},
+ },
+ {
+ name: "keys",
+ method: http.MethodGet,
+ path: testProvider.KeysEndpoint().Relative(),
+ wantCode: http.StatusOK,
+ contains: []string{
+ `{"keys":[{"use":"sig","kty":"RSA","kid":"`,
+ `","alg":"RS256","n":"`, `","e":"AQAB"}]}`,
+ },
+ },
+ {
+ name: "device authorization",
+ method: http.MethodGet,
+ path: testProvider.DeviceAuthorizationEndpoint().Relative(),
+ basicAuth: &basicAuth{"web", "secret"},
+ values: map[string]string{
+ "scope": oidc.SpaceDelimitedArray{oidc.ScopeOpenID, oidc.ScopeOfflineAccess}.Encode(),
+ },
+ wantCode: http.StatusOK,
+ contains: []string{
+ `{"device_code":"`, `","user_code":"`,
+ `","verification_uri":"https://localhost:9998/device"`,
+ `"verification_uri_complete":"https://localhost:9998/device?user_code=`,
+ `","expires_in":300,"interval":5}`,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ u := gu.PtrCopy(serverURL)
+ u.Path = tt.path
+ if tt.values != nil {
+ u.RawQuery = mapAsValues(tt.values)
+ }
+ var body io.Reader
+ if tt.body != nil {
+ body = strings.NewReader(mapAsValues(tt.body))
+ }
+
+ req := httptest.NewRequest(tt.method, u.String(), body)
+ for k, v := range tt.header {
+ req.Header.Set(k, v)
+ }
+ if tt.basicAuth != nil {
+ req.SetBasicAuth(tt.basicAuth.username, tt.basicAuth.password)
+ }
+
+ rec := httptest.NewRecorder()
+ testProvider.HttpHandler().ServeHTTP(rec, req)
+
+ resp := rec.Result()
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantCode, resp.StatusCode)
+
+ respBody, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ respBodyString := string(respBody)
+ t.Log(respBodyString)
+ t.Log(resp.Header)
+
+ if tt.json != "" {
+ assert.JSONEq(t, tt.json, respBodyString)
+ }
+ for _, c := range tt.contains {
+ assert.Contains(t, respBodyString, c)
+ }
+ for k, v := range tt.headerContains {
+ assert.Contains(t, resp.Header.Get(k), v)
+ }
+ })
+ }
+}
diff --git a/pkg/op/probes.go b/pkg/op/probes.go
index 7b80fb4..a56c92b 100644
--- a/pkg/op/probes.go
+++ b/pkg/op/probes.go
@@ -5,7 +5,7 @@ import (
"errors"
"net/http"
- httphelper "github.com/zitadel/oidc/pkg/http"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
)
type ProbesFn func(context.Context) error
@@ -31,15 +31,6 @@ func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) {
ok(w)
}
-func ReadySigner(s Signer) ProbesFn {
- return func(ctx context.Context) error {
- if s == nil {
- return errors.New("no signer")
- }
- return s.Health(ctx)
- }
-}
-
func ReadyStorage(s Storage) ProbesFn {
return func(ctx context.Context) error {
if s == nil {
diff --git a/pkg/op/session.go b/pkg/op/session.go
index 737bb86..c4f76f3 100644
--- a/pkg/op/session.go
+++ b/pkg/op/session.go
@@ -6,14 +6,14 @@ import (
"net/url"
"path"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type SessionEnder interface {
Decoder() httphelper.Decoder
Storage() Storage
- IDTokenHintVerifier() IDTokenHintVerifier
+ IDTokenHintVerifier(context.Context) IDTokenHintVerifier
DefaultLogoutRedirectURI() string
}
@@ -60,7 +60,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
RedirectURI: ender.DefaultLogoutRedirectURI(),
}
if req.IdTokenHint != "" {
- claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier())
+ 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/signer.go b/pkg/op/signer.go
index 828876e..22ef8ca 100644
--- a/pkg/op/signer.go
+++ b/pkg/op/signer.go
@@ -1,88 +1,38 @@
package op
import (
- "context"
"errors"
- "sync"
- "github.com/zitadel/logging"
"gopkg.in/square/go-jose.v2"
)
-type Signer interface {
- Health(ctx context.Context) error
- Signer() jose.Signer
+var (
+ ErrSignerCreationFailed = errors.New("signer creation failed")
+)
+
+type SigningKey interface {
SignatureAlgorithm() jose.SignatureAlgorithm
+ Key() interface{}
+ ID() string
}
-type tokenSigner struct {
- signer jose.Signer
- storage AuthStorage
- alg jose.SignatureAlgorithm
- lock sync.RWMutex
-}
-
-func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.SigningKey) Signer {
- s := &tokenSigner{
- storage: storage,
- }
-
- select {
- case <-ctx.Done():
- return nil
- case key := <-keyCh:
- s.exchangeSigningKey(key)
- }
- go s.refreshSigningKey(ctx, keyCh)
-
- return s
-}
-
-func (s *tokenSigner) Health(_ context.Context) error {
- if s.signer == nil {
- return errors.New("no signer")
- }
- if string(s.alg) == "" {
- return errors.New("no signing algorithm")
- }
- return nil
-}
-
-func (s *tokenSigner) Signer() jose.Signer {
- s.lock.RLock()
- defer s.lock.RUnlock()
- return s.signer
-}
-
-func (s *tokenSigner) refreshSigningKey(ctx context.Context, keyCh <-chan jose.SigningKey) {
- for {
- select {
- case <-ctx.Done():
- return
- case key := <-keyCh:
- s.exchangeSigningKey(key)
- }
- }
-}
-
-func (s *tokenSigner) exchangeSigningKey(key jose.SigningKey) {
- s.lock.Lock()
- defer s.lock.Unlock()
- s.alg = key.Algorithm
- if key.Algorithm == "" || key.Key == nil {
- s.signer = nil
- logging.Warn("signer has no key")
- return
- }
- var err error
- s.signer, err = jose.NewSigner(key, &jose.SignerOptions{})
+func SignerFromKey(key SigningKey) (jose.Signer, error) {
+ signer, err := jose.NewSigner(jose.SigningKey{
+ Algorithm: key.SignatureAlgorithm(),
+ Key: &jose.JSONWebKey{
+ Key: key.Key(),
+ KeyID: key.ID(),
+ },
+ }, &jose.SignerOptions{})
if err != nil {
- logging.New().WithError(err).Error("error creating signer")
- return
+ return nil, ErrSignerCreationFailed //TODO: log / wrap error?
}
- logging.Info("signer exchanged signing key")
+ return signer, nil
}
-func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
- return s.alg
+type Key interface {
+ ID() string
+ Algorithm() jose.SignatureAlgorithm
+ Use() string
+ Key() interface{}
}
diff --git a/pkg/op/storage.go b/pkg/op/storage.go
index 28fc6a3..e36eac7 100644
--- a/pkg/op/storage.go
+++ b/pkg/op/storage.go
@@ -7,7 +7,7 @@ import (
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type AuthStorage interface {
@@ -25,6 +25,8 @@ type AuthStorage interface {
//
// * *oidc.JWTTokenRequest from a JWT that is the assertion value of a JWT Profile
// Grant: https://datatracker.ietf.org/doc/html/rfc7523#section-2.1
+ //
+ // * TokenExchangeRequest as returned by ValidateTokenExchangeRequest
CreateAccessToken(context.Context, TokenRequest) (accessTokenID string, expiration time.Time, err error)
// The TokenRequest parameter of CreateAccessAndRefreshTokens can be any of:
@@ -36,6 +38,8 @@ type AuthStorage interface {
// * AuthRequest as by returned by the AuthRequestByID or AuthRequestByCode (above).
// Used for the authorization code flow which requested offline_access scope and
// registered the refresh_token grant type in advance
+ //
+ // * TokenExchangeRequest as returned by ValidateTokenExchangeRequest
CreateAccessAndRefreshTokens(ctx context.Context, request TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshTokenID string, expiration time.Time, err error)
TokenRequestByRefreshToken(ctx context.Context, refreshTokenID string) (RefreshTokenRequest, error)
@@ -44,44 +48,85 @@ type AuthStorage interface {
// RevokeToken should revoke a token. In the situation that the original request was to
// revoke an access token, then tokenOrTokenID will be a tokenID and userID will be set
// but if the original request was for a refresh token, then userID will be empty and
- // tokenOrTokenID will be the refresh token, not its ID.
+ // tokenOrTokenID will be the refresh token, not its ID. RevokeToken depends upon GetRefreshTokenInfo
+ // to get information from refresh tokens that are not either ":" strings
+ // nor JWTs.
RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error
- GetSigningKey(context.Context, chan<- jose.SigningKey)
- GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
-}
-
-// CanRefreshTokenInfo is an optional additional interface that Storage can support.
-// Supporting CanRefreshTokenInfo is required to be able to (revoke) a refresh token that
-// is neither an encrypted string of : nor a JWT.
-type CanRefreshTokenInfo interface {
// GetRefreshTokenInfo must return ErrInvalidRefreshToken when presented
// with a token that is not a refresh token.
GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error)
+
+ SigningKey(context.Context) (SigningKey, error)
+ SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
+ KeySet(context.Context) ([]Key, error)
+}
+
+type ClientCredentialsStorage interface {
+ ClientCredentials(ctx context.Context, clientID, clientSecret string) (Client, error)
+ ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
+}
+
+type TokenExchangeStorage interface {
+ // ValidateTokenExchangeRequest will be called to validate parsed (including tokens) Token Exchange Grant request.
+ //
+ // Important validations can include:
+ // - permissions
+ // - set requested token type to some default value if it is empty (rfc 8693 allows it) using SetRequestedTokenType method.
+ // Depending on RequestedTokenType - the following tokens will be issued:
+ // - RefreshTokenType - both access and refresh tokens
+ // - AccessTokenType - only access token
+ // - IDTokenType - only id token
+ // - validation of subject's token type on possibility to be exchanged to the requested token type (according to your requirements)
+ // - scopes (and update them using SetCurrentScopes method)
+ // - set new subject if it differs from exchange subject (impersonation flow)
+ //
+ // Request will include subject's and/or actor's token claims if correspinding tokens are access/id_token issued by op
+ // or third party tokens parsed by TokenExchangeTokensVerifierStorage interface methods.
+ ValidateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error
+
+ // CreateTokenExchangeRequest will be called after parsing and validating token exchange request.
+ // Stored request is not accessed later by op - so it is up to implementer to decide
+ // should this method actually store the request or not (common use case - store for it for audit purposes)
+ CreateTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) error
+
+ // GetPrivateClaimsFromTokenExchangeRequest will be called during access token creation.
+ // Claims evaluation can be based on all validated request data available, including: scopes, resource, audience, etc.
+ GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request TokenExchangeRequest) (claims map[string]interface{}, err error)
+
+ // 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.UserInfo, request TokenExchangeRequest) error
+}
+
+// TokenExchangeTokensVerifierStorage is an optional interface used in token exchange process to verify tokens
+// issued by third-party applications. If interface is not implemented - only tokens issued by op will be exchanged.
+type TokenExchangeTokensVerifierStorage interface {
+ VerifyExchangeSubjectToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, subject string, tokenClaims map[string]interface{}, err error)
+ VerifyExchangeActorToken(ctx context.Context, token string, tokenType oidc.TokenType) (tokenIDOrToken string, actor string, tokenClaims map[string]interface{}, err error)
}
var ErrInvalidRefreshToken = errors.New("invalid_refresh_token")
-type ClientCredentialsStorage interface {
- ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
-}
-
type OPStorage interface {
// GetClientByClientID loads a Client. The returned Client is never cached and is only used to
// handle the current request.
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)
-
- // GetKeyByIDAndUserID is mis-named. It does not pass userID. Instead
- // it passes the clientID.
- GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
+ GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error)
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error)
}
+// JWTProfileTokenStorage is an additional, optional storage to implement
+// implementing it, allows specifying the [AccessTokenType] of the access_token returned form the JWT Profile TokenRequest
+type JWTProfileTokenStorage interface {
+ JWTProfileTokenType(ctx context.Context, request TokenRequest) (AccessTokenType, error)
+}
+
// Storage is a required parameter for NewOpenIDProvider(). In addition to the
// embedded interfaces below, if the passed Storage implements ClientCredentialsStorage
// then the grant type "client_credentials" will be supported. In that case, the access
@@ -102,3 +147,50 @@ type EndSessionRequest struct {
ClientID string
RedirectURI string
}
+
+var ErrDuplicateUserCode = errors.New("user code already exists")
+
+type DeviceAuthorizationState struct {
+ ClientID string
+ Scopes []string
+ Expires time.Time
+ Done bool
+ Subject string
+ Denied bool
+}
+
+type DeviceAuthorizationStorage interface {
+ // StoreDeviceAuthorizationRequest stores a new device authorization request in the database.
+ // User code will be used by the user to complete the login flow and must be unique.
+ // ErrDuplicateUserCode signals the caller should try again with a new code.
+ //
+ // Note that user codes are low entropy keys and when many exist in the
+ // database, the change for collisions increases. Therefore implementers
+ // of this interface must make sure that user codes of expired authentication flows are purged,
+ // after some time.
+ StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error
+
+ // GetDeviceAuthorizatonState returns the current state of the device authorization flow in the database.
+ // The method is polled untill the the authorization is eighter Completed, Expired or Denied.
+ GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*DeviceAuthorizationState, error)
+
+ // GetDeviceAuthorizationByUserCode resturn the current state of the device authorization flow,
+ // identified by the user code.
+ GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*DeviceAuthorizationState, error)
+
+ // CompleteDeviceAuthorization marks a device authorization entry as Completed,
+ // identified by userCode. The Subject is added to the state, so that
+ // GetDeviceAuthorizatonState can use it to create a new Access Token.
+ CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error
+
+ // DenyDeviceAuthorization marks a device authorization entry as Denied.
+ DenyDeviceAuthorization(ctx context.Context, userCode string) error
+}
+
+func assertDeviceStorage(s Storage) (DeviceAuthorizationStorage, error) {
+ storage, ok := s.(DeviceAuthorizationStorage)
+ if !ok {
+ return nil, oidc.ErrUnsupportedGrantType().WithDescription("device_code grant not supported")
+ }
+ return storage, nil
+}
diff --git a/pkg/op/token.go b/pkg/op/token.go
index 3a72261..58568a7 100644
--- a/pkg/op/token.go
+++ b/pkg/op/token.go
@@ -4,14 +4,12 @@ import (
"context"
"time"
- "github.com/zitadel/oidc/pkg/crypto"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/strings"
+ "github.com/zitadel/oidc/v2/pkg/crypto"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/strings"
)
type TokenCreator interface {
- Issuer() string
- Signer() Signer
Storage() Storage
Crypto() Crypto
}
@@ -22,6 +20,13 @@ type TokenRequest interface {
GetScopes() []string
}
+type AccessTokenClient interface {
+ GetID() string
+ ClockSkew() time.Duration
+ RestrictAdditionalAccessTokenScopes() func(scopes []string) []string
+ GrantTypes() []oidc.GrantType
+}
+
func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) {
var accessToken, newRefreshToken string
var validity time.Duration
@@ -32,7 +37,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
return nil, err
}
}
- idToken, err := CreateIDToken(ctx, creator.Issuer(), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), creator.Signer(), client)
+ idToken, err := CreateIDToken(ctx, IssuerFromContext(ctx), request, client.IDTokenLifetime(), accessToken, code, creator.Storage(), client)
if err != nil {
return nil, err
}
@@ -57,7 +62,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
}, nil
}
-func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client Client) (id, newRefreshToken string, exp time.Time, err error) {
+func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storage, refreshToken string, client AccessTokenClient) (id, newRefreshToken string, exp time.Time, err error) {
if needsRefreshToken(tokenRequest, client) {
return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken)
}
@@ -65,10 +70,12 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag
return
}
-func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
+func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
switch req := tokenRequest.(type) {
case AuthRequest:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
+ case TokenExchangeRequest:
+ return req.GetRequestedTokenType() == oidc.RefreshTokenType
case RefreshTokenRequest:
return true
default:
@@ -76,7 +83,7 @@ func needsRefreshToken(tokenRequest TokenRequest, client Client) bool {
}
}
-func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client Client, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
+func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTokenType AccessTokenType, creator TokenCreator, client AccessTokenClient, refreshToken string) (accessToken, newRefreshToken string, validity time.Duration, err error) {
id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client)
if err != nil {
return "", "", 0, err
@@ -87,7 +94,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok
}
validity = exp.Add(clockSkew).Sub(time.Now().UTC())
if accessTokenType == AccessTokenTypeJWT {
- accessToken, err = CreateJWT(ctx, creator.Issuer(), tokenRequest, exp, id, creator.Signer(), client, creator.Storage())
+ accessToken, err = CreateJWT(ctx, IssuerFromContext(ctx), tokenRequest, exp, id, client, creator.Storage())
return
}
accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
@@ -98,17 +105,41 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) {
return crypto.Encrypt(tokenID + ":" + subject)
}
-func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, signer Signer, client Client, storage Storage) (string, error) {
+func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, exp time.Time, id string, client AccessTokenClient, storage Storage) (string, error) {
claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew())
if client != nil {
restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes())
- privateClaims, err := storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes))
+
+ var (
+ privateClaims map[string]interface{}
+ err error
+ )
+
+ tokenExchangeRequest, okReq := tokenRequest.(TokenExchangeRequest)
+ teStorage, okStorage := storage.(TokenExchangeStorage)
+ if okReq && okStorage {
+ privateClaims, err = teStorage.GetPrivateClaimsFromTokenExchangeRequest(
+ ctx,
+ tokenExchangeRequest,
+ )
+ } else {
+ privateClaims, err = storage.GetPrivateClaimsFromScopes(ctx, tokenRequest.GetSubject(), client.GetID(), removeUserinfoScopes(restrictedScopes))
+ }
+
if err != nil {
return "", err
}
- claims.SetPrivateClaims(privateClaims)
+ claims.Claims = privateClaims
}
- return crypto.Sign(claims, signer.Signer())
+ signingKey, err := storage.SigningKey(ctx)
+ if err != nil {
+ return "", err
+ }
+ signer, err := SignerFromKey(signingKey)
+ if err != nil {
+ return "", err
+ }
+ return crypto.Sign(claims, signer)
}
type IDTokenRequest interface {
@@ -120,7 +151,7 @@ type IDTokenRequest interface {
GetSubject() string
}
-func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, signer Signer, client Client) (string, error) {
+func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, validity time.Duration, accessToken, code string, storage Storage, client Client) (string, error) {
exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity)
var acr, nonce string
if authRequest, ok := request.(AuthRequest); ok {
@@ -129,33 +160,50 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v
}
claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew())
scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes())
+ signingKey, err := storage.SigningKey(ctx)
+ if err != nil {
+ return "", err
+ }
if accessToken != "" {
- atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm())
+ atHash, err := oidc.ClaimHash(accessToken, signingKey.SignatureAlgorithm())
if err != nil {
return "", err
}
- claims.SetAccessTokenHash(atHash)
+ claims.AccessTokenHash = atHash
if !client.IDTokenUserinfoClaimsAssertion() {
scopes = removeUserinfoScopes(scopes)
}
}
- if len(scopes) > 0 {
- userInfo := oidc.NewUserInfo()
+
+ tokenExchangeRequest, okReq := request.(TokenExchangeRequest)
+ teStorage, okStorage := storage.(TokenExchangeStorage)
+ if okReq && okStorage {
+ userInfo := new(oidc.UserInfo)
+ err := teStorage.SetUserinfoFromTokenExchangeRequest(ctx, userInfo, tokenExchangeRequest)
+ if err != nil {
+ return "", err
+ }
+ claims.SetUserInfo(userInfo)
+ } else if len(scopes) > 0 {
+ 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, signer.SignatureAlgorithm())
+ codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm())
if err != nil {
return "", err
}
- claims.SetCodeHash(codeHash)
+ claims.CodeHash = codeHash
}
-
- return crypto.Sign(claims, signer.Signer())
+ signer, err := SignerFromKey(signingKey)
+ if err != nil {
+ return "", err
+ }
+ return crypto.Sign(claims, signer)
}
func removeUserinfoScopes(scopes []string) []string {
diff --git a/pkg/op/token_client_credentials.go b/pkg/op/token_client_credentials.go
index 3787667..fc31d57 100644
--- a/pkg/op/token_client_credentials.go
+++ b/pkg/op/token_client_credentials.go
@@ -5,8 +5,8 @@ import (
"net/http"
"net/url"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
// ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including
@@ -63,15 +63,15 @@ func ParseClientCredentialsRequest(r *http.Request, decoder httphelper.Decoder)
return request, nil
}
-// ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client
-// and returns the data representing the original auth request corresponding to the refresh_token
+// ValidateClientCredentialsRequest validates the client_credentials request parameters including authorization check of the client
+// and returns a TokenRequest and Client implementation to be used in the client_credentials response, resp. creation of the corresponding access_token.
func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) {
storage, ok := exchanger.Storage().(ClientCredentialsStorage)
if !ok {
return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported")
}
- client, err := AuthorizeClientCredentialsClient(ctx, request, exchanger)
+ client, err := AuthorizeClientCredentialsClient(ctx, request, storage)
if err != nil {
return nil, nil, err
}
@@ -84,12 +84,8 @@ func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientC
return tokenRequest, client, nil
}
-func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (Client, error) {
- if err := AuthorizeClientIDSecret(ctx, request.ClientID, request.ClientSecret, exchanger.Storage()); err != nil {
- return nil, err
- }
-
- client, err := exchanger.Storage().GetClientByClientID(ctx, request.ClientID)
+func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, storage ClientCredentialsStorage) (Client, error) {
+ client, err := storage.ClientCredentials(ctx, request.ClientID, request.ClientSecret)
if err != nil {
return nil, oidc.ErrInvalidClient().WithParent(err)
}
@@ -102,7 +98,7 @@ func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientC
}
func CreateClientCredentialsTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) {
- accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, AccessTokenTypeJWT, creator, client, "")
+ accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "")
if err != nil {
return nil, err
}
diff --git a/pkg/op/token_code.go b/pkg/op/token_code.go
index ec48233..565a477 100644
--- a/pkg/op/token_code.go
+++ b/pkg/op/token_code.go
@@ -4,8 +4,8 @@ import (
"context"
"net/http"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
// CodeExchange handles the OAuth 2.0 authorization_code grant, including
diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go
index 7bb6e42..055ff13 100644
--- a/pkg/op/token_exchange.go
+++ b/pkg/op/token_exchange.go
@@ -1,11 +1,399 @@
package op
import (
- "errors"
+ "context"
"net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
-// TokenExchange will handle the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange")
-func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
- RequestError(w, r, errors.New("unimplemented"))
+type TokenExchangeRequest interface {
+ GetAMR() []string
+ GetAudience() []string
+ GetResourses() []string
+ GetAuthTime() time.Time
+ GetClientID() string
+ GetScopes() []string
+ GetSubject() string
+ GetRequestedTokenType() oidc.TokenType
+
+ GetExchangeSubject() string
+ GetExchangeSubjectTokenType() oidc.TokenType
+ GetExchangeSubjectTokenIDOrToken() string
+ GetExchangeSubjectTokenClaims() map[string]interface{}
+
+ GetExchangeActor() string
+ GetExchangeActorTokenType() oidc.TokenType
+ GetExchangeActorTokenIDOrToken() string
+ GetExchangeActorTokenClaims() map[string]interface{}
+
+ SetCurrentScopes(scopes []string)
+ SetRequestedTokenType(tt oidc.TokenType)
+ SetSubject(subject string)
+}
+
+type tokenExchangeRequest struct {
+ exchangeSubjectTokenIDOrToken string
+ exchangeSubjectTokenType oidc.TokenType
+ exchangeSubject string
+ exchangeSubjectTokenClaims map[string]interface{}
+
+ exchangeActorTokenIDOrToken string
+ exchangeActorTokenType oidc.TokenType
+ exchangeActor string
+ exchangeActorTokenClaims map[string]interface{}
+
+ resource []string
+ audience oidc.Audience
+ scopes oidc.SpaceDelimitedArray
+ requestedTokenType oidc.TokenType
+ clientID string
+ authTime time.Time
+ subject string
+}
+
+func (r *tokenExchangeRequest) GetAMR() []string {
+ return []string{}
+}
+
+func (r *tokenExchangeRequest) GetAudience() []string {
+ return r.audience
+}
+
+func (r *tokenExchangeRequest) GetResourses() []string {
+ return r.resource
+}
+
+func (r *tokenExchangeRequest) GetAuthTime() time.Time {
+ return r.authTime
+}
+
+func (r *tokenExchangeRequest) GetClientID() string {
+ return r.clientID
+}
+
+func (r *tokenExchangeRequest) GetScopes() []string {
+ return r.scopes
+}
+
+func (r *tokenExchangeRequest) GetRequestedTokenType() oidc.TokenType {
+ return r.requestedTokenType
+}
+
+func (r *tokenExchangeRequest) GetExchangeSubject() string {
+ return r.exchangeSubject
+}
+
+func (r *tokenExchangeRequest) GetExchangeSubjectTokenType() oidc.TokenType {
+ return r.exchangeSubjectTokenType
+}
+
+func (r *tokenExchangeRequest) GetExchangeSubjectTokenIDOrToken() string {
+ return r.exchangeSubjectTokenIDOrToken
+}
+
+func (r *tokenExchangeRequest) GetExchangeSubjectTokenClaims() map[string]interface{} {
+ return r.exchangeSubjectTokenClaims
+}
+
+func (r *tokenExchangeRequest) GetExchangeActor() string {
+ return r.exchangeActor
+}
+
+func (r *tokenExchangeRequest) GetExchangeActorTokenType() oidc.TokenType {
+ return r.exchangeActorTokenType
+}
+
+func (r *tokenExchangeRequest) GetExchangeActorTokenIDOrToken() string {
+ return r.exchangeActorTokenIDOrToken
+}
+
+func (r *tokenExchangeRequest) GetExchangeActorTokenClaims() map[string]interface{} {
+ return r.exchangeActorTokenClaims
+}
+
+func (r *tokenExchangeRequest) GetSubject() string {
+ return r.subject
+}
+
+func (r *tokenExchangeRequest) SetCurrentScopes(scopes []string) {
+ r.scopes = scopes
+}
+
+func (r *tokenExchangeRequest) SetRequestedTokenType(tt oidc.TokenType) {
+ r.requestedTokenType = tt
+}
+
+func (r *tokenExchangeRequest) SetSubject(subject string) {
+ r.subject = subject
+}
+
+// TokenExchange handles the OAuth 2.0 token exchange grant ("urn:ietf:params:oauth:grant-type:token-exchange")
+func TokenExchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
+ tokenExchangeReq, clientID, clientSecret, err := ParseTokenExchangeRequest(r, exchanger.Decoder())
+ if err != nil {
+ RequestError(w, r, err)
+ }
+
+ tokenExchangeRequest, client, err := ValidateTokenExchangeRequest(r.Context(), tokenExchangeReq, clientID, clientSecret, exchanger)
+ if err != nil {
+ RequestError(w, r, err)
+ return
+ }
+ resp, err := CreateTokenExchangeResponse(r.Context(), tokenExchangeRequest, client, exchanger)
+ if err != nil {
+ RequestError(w, r, err)
+ return
+ }
+ httphelper.MarshalJSON(w, resp)
+}
+
+// ParseTokenExchangeRequest parses the http request into oidc.TokenExchangeRequest
+func ParseTokenExchangeRequest(r *http.Request, decoder httphelper.Decoder) (_ *oidc.TokenExchangeRequest, clientID, clientSecret string, err error) {
+ err = r.ParseForm()
+ if err != nil {
+ return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error parsing form").WithParent(err)
+ }
+
+ request := new(oidc.TokenExchangeRequest)
+ err = decoder.Decode(request, r.Form)
+ if err != nil {
+ return nil, "", "", oidc.ErrInvalidRequest().WithDescription("error decoding form").WithParent(err)
+ }
+
+ var ok bool
+ if clientID, clientSecret, ok = r.BasicAuth(); ok {
+ clientID, err = url.QueryUnescape(clientID)
+ if err != nil {
+ return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err)
+ }
+
+ clientSecret, err = url.QueryUnescape(clientSecret)
+ if err != nil {
+ return nil, "", "", oidc.ErrInvalidClient().WithDescription("invalid basic auth header").WithParent(err)
+ }
+ }
+
+ return request, clientID, clientSecret, nil
+}
+
+// ValidateTokenExchangeRequest validates the token exchange request parameters including authorization check of the client,
+// subject_token and actor_token
+func ValidateTokenExchangeRequest(
+ ctx context.Context,
+ oidcTokenExchangeRequest *oidc.TokenExchangeRequest,
+ clientID, clientSecret string,
+ exchanger Exchanger,
+) (TokenExchangeRequest, Client, error) {
+ if oidcTokenExchangeRequest.SubjectToken == "" {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token missing")
+ }
+
+ if oidcTokenExchangeRequest.SubjectTokenType == "" {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type missing")
+ }
+
+ storage := exchanger.Storage()
+ teStorage, ok := storage.(TokenExchangeStorage)
+ if !ok {
+ return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("token_exchange grant not supported")
+ }
+
+ client, err := AuthorizeTokenExchangeClient(ctx, clientID, clientSecret, exchanger)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if oidcTokenExchangeRequest.RequestedTokenType != "" && !oidcTokenExchangeRequest.RequestedTokenType.IsSupported() {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("requested_token_type is not supported")
+ }
+
+ if !oidcTokenExchangeRequest.SubjectTokenType.IsSupported() {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token_type is not supported")
+ }
+
+ if oidcTokenExchangeRequest.ActorTokenType != "" && !oidcTokenExchangeRequest.ActorTokenType.IsSupported() {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token_type is not supported")
+ }
+
+ exchangeSubjectTokenIDOrToken, exchangeSubject, exchangeSubjectTokenClaims, ok := GetTokenIDAndSubjectFromToken(ctx, exchanger,
+ oidcTokenExchangeRequest.SubjectToken, oidcTokenExchangeRequest.SubjectTokenType, false)
+ if !ok {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("subject_token is invalid")
+ }
+
+ var (
+ exchangeActorTokenIDOrToken, exchangeActor string
+ exchangeActorTokenClaims map[string]interface{}
+ )
+ if oidcTokenExchangeRequest.ActorToken != "" {
+ exchangeActorTokenIDOrToken, exchangeActor, exchangeActorTokenClaims, ok = GetTokenIDAndSubjectFromToken(ctx, exchanger,
+ oidcTokenExchangeRequest.ActorToken, oidcTokenExchangeRequest.ActorTokenType, true)
+ if !ok {
+ return nil, nil, oidc.ErrInvalidRequest().WithDescription("actor_token is invalid")
+ }
+ }
+
+ req := &tokenExchangeRequest{
+ exchangeSubjectTokenIDOrToken: exchangeSubjectTokenIDOrToken,
+ exchangeSubjectTokenType: oidcTokenExchangeRequest.SubjectTokenType,
+ exchangeSubject: exchangeSubject,
+ exchangeSubjectTokenClaims: exchangeSubjectTokenClaims,
+
+ exchangeActorTokenIDOrToken: exchangeActorTokenIDOrToken,
+ exchangeActorTokenType: oidcTokenExchangeRequest.ActorTokenType,
+ exchangeActor: exchangeActor,
+ exchangeActorTokenClaims: exchangeActorTokenClaims,
+
+ subject: exchangeSubject,
+ resource: oidcTokenExchangeRequest.Resource,
+ audience: oidcTokenExchangeRequest.Audience,
+ scopes: oidcTokenExchangeRequest.Scopes,
+ requestedTokenType: oidcTokenExchangeRequest.RequestedTokenType,
+ clientID: client.GetID(),
+ authTime: time.Now(),
+ }
+
+ err = teStorage.ValidateTokenExchangeRequest(ctx, req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ err = teStorage.CreateTokenExchangeRequest(ctx, req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return req, client, nil
+}
+
+func GetTokenIDAndSubjectFromToken(
+ ctx context.Context,
+ exchanger Exchanger,
+ token string,
+ tokenType oidc.TokenType,
+ isActor bool,
+) (tokenIDOrToken, subject string, claims map[string]interface{}, ok bool) {
+ switch tokenType {
+ case oidc.AccessTokenType:
+ var accessTokenClaims *oidc.AccessTokenClaims
+ tokenIDOrToken, subject, accessTokenClaims, ok = getTokenIDAndClaims(ctx, exchanger, token)
+ claims = accessTokenClaims.Claims
+ case oidc.RefreshTokenType:
+ refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token)
+ if err != nil {
+ break
+ }
+
+ tokenIDOrToken, subject, ok = token, refreshTokenRequest.GetSubject(), true
+ case oidc.IDTokenType:
+ idTokenClaims, err := VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, token, exchanger.IDTokenHintVerifier(ctx))
+ if err != nil {
+ break
+ }
+
+ tokenIDOrToken, subject, claims, ok = token, idTokenClaims.Subject, idTokenClaims.Claims, true
+ }
+
+ if !ok {
+ if verifier, ok := exchanger.Storage().(TokenExchangeTokensVerifierStorage); ok {
+ var err error
+ if isActor {
+ tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeActorToken(ctx, token, tokenType)
+ } else {
+ tokenIDOrToken, subject, claims, err = verifier.VerifyExchangeSubjectToken(ctx, token, tokenType)
+ }
+ if err != nil {
+ return "", "", nil, false
+ }
+
+ return tokenIDOrToken, subject, claims, true
+ }
+
+ return "", "", nil, false
+ }
+
+ return tokenIDOrToken, subject, claims, true
+}
+
+// AuthorizeTokenExchangeClient authorizes a client by validating the client_id and client_secret
+func AuthorizeTokenExchangeClient(ctx context.Context, clientID, clientSecret string, exchanger Exchanger) (client Client, err error) {
+ if err := AuthorizeClientIDSecret(ctx, clientID, clientSecret, exchanger.Storage()); err != nil {
+ return nil, err
+ }
+
+ client, err = exchanger.Storage().GetClientByClientID(ctx, clientID)
+ if err != nil {
+ return nil, oidc.ErrInvalidClient().WithParent(err)
+ }
+
+ return client, nil
+}
+
+func CreateTokenExchangeResponse(
+ ctx context.Context,
+ tokenExchangeRequest TokenExchangeRequest,
+ client Client,
+ creator TokenCreator,
+) (_ *oidc.TokenExchangeResponse, err error) {
+
+ var (
+ token, refreshToken, tokenType string
+ validity time.Duration
+ )
+
+ switch tokenExchangeRequest.GetRequestedTokenType() {
+ case oidc.AccessTokenType, oidc.RefreshTokenType:
+ token, refreshToken, validity, err = CreateAccessToken(ctx, tokenExchangeRequest, client.AccessTokenType(), creator, client, "")
+ if err != nil {
+ return nil, err
+ }
+
+ tokenType = oidc.BearerToken
+ case oidc.IDTokenType:
+ token, err = CreateIDToken(ctx, IssuerFromContext(ctx), tokenExchangeRequest, client.IDTokenLifetime(), "", "", creator.Storage(), client)
+ if err != nil {
+ return nil, err
+ }
+
+ // not applicable (see https://datatracker.ietf.org/doc/html/rfc8693#section-2-2-1-2-6)
+ tokenType = "N_A"
+ default:
+ // oidc.JWTTokenType and other custom token types are not supported for issuing.
+ // In the future it can be considered to have custom tokens generation logic injected via op configuration
+ // or via expanding Storage interface
+ oidc.ErrInvalidRequest().WithDescription("requested_token_type is invalid")
+ }
+
+ exp := uint64(validity.Seconds())
+ return &oidc.TokenExchangeResponse{
+ AccessToken: token,
+ IssuedTokenType: tokenExchangeRequest.GetRequestedTokenType(),
+ TokenType: tokenType,
+ ExpiresIn: exp,
+ RefreshToken: refreshToken,
+ Scopes: tokenExchangeRequest.GetScopes(),
+ }, nil
+}
+
+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, ":")
+ if len(splitToken) != 2 {
+ return "", "", nil, false
+ }
+
+ return splitToken[0], splitToken[1], nil, true
+ }
+ accessTokenClaims, err := VerifyAccessToken[*oidc.AccessTokenClaims](ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
+ if err != nil {
+ return "", "", nil, false
+ }
+
+ return accessTokenClaims.JWTID, accessTokenClaims.Subject, accessTokenClaims, true
}
diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go
index f402c8b..8582388 100644
--- a/pkg/op/token_intospection.go
+++ b/pkg/op/token_intospection.go
@@ -1,24 +1,24 @@
package op
import (
+ "context"
"errors"
"net/http"
- "net/url"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type Introspector interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
- AccessTokenVerifier() AccessTokenVerifier
+ AccessTokenVerifier(context.Context) AccessTokenVerifier
}
type IntrospectorJWTProfile interface {
Introspector
- JWTProfileVerifier() JWTProfileVerifier
+ JWTProfileVerifier(context.Context) JWTProfileVerifier
}
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
@@ -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,43 +44,24 @@ 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)
}
func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
- err = r.ParseForm()
+ clientID, authenticated, err := ClientIDFromRequest(r, introspector)
if err != nil {
- return "", "", errors.New("unable to parse request")
+ return "", "", err
}
- req := new(struct {
- oidc.IntrospectionRequest
- oidc.ClientAssertionParams
- })
+ if !authenticated {
+ return "", "", oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials)
+ }
+
+ req := new(oidc.IntrospectionRequest)
err = introspector.Decoder().Decode(req, r.Form)
if err != nil {
return "", "", errors.New("unable to parse request")
}
- if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
- profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier())
- if err == nil {
- return req.Token, profile.Issuer, nil
- }
- }
- clientID, clientSecret, ok := r.BasicAuth()
- if ok {
- clientID, err = url.QueryUnescape(clientID)
- if err != nil {
- return "", "", errors.New("invalid basic auth header")
- }
- clientSecret, err = url.QueryUnescape(clientSecret)
- if err != nil {
- return "", "", errors.New("invalid basic auth header")
- }
- if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
- return "", "", err
- }
- return req.Token, clientID, nil
- }
- return "", "", errors.New("invalid authorization")
+
+ return req.Token, clientID, nil
}
diff --git a/pkg/op/token_jwt_profile.go b/pkg/op/token_jwt_profile.go
index eb21517..23bac9a 100644
--- a/pkg/op/token_jwt_profile.go
+++ b/pkg/op/token_jwt_profile.go
@@ -5,13 +5,13 @@ import (
"net/http"
"time"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type JWTAuthorizationGrantExchanger interface {
Exchanger
- JWTProfileVerifier() JWTProfileVerifier
+ JWTProfileVerifier(context.Context) JWTProfileVerifier
}
// JWTProfile handles the OAuth 2.0 JWT Profile Authorization Grant https://tools.ietf.org/html/rfc7523#section-2.1
@@ -21,7 +21,7 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati
RequestError(w, r, err)
}
- tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier())
+ tokenRequest, err := VerifyJWTAssertion(r.Context(), profileRequest.Assertion, exchanger.JWTProfileVerifier(r.Context()))
if err != nil {
RequestError(w, r, err)
return
@@ -53,27 +53,65 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (*
return tokenReq, nil
}
-// CreateJWTTokenResponse creates
+// CreateJWTTokenResponse creates an access_token response for a JWT Profile Grant request
+// by default the access_token is an opaque string, but can be specified by implementing the JWTProfileTokenStorage interface
func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
- id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest)
- if err != nil {
- return nil, err
- }
- accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
- if err != nil {
- return nil, err
+ // return an opaque token as default to not break current implementations
+ tokenType := AccessTokenTypeBearer
+
+ // the current CreateAccessToken function, esp. CreateJWT requires an implementation of an AccessTokenClient
+ client := &jwtProfileClient{
+ id: tokenRequest.GetSubject(),
}
+ // by implementing the JWTProfileTokenStorage the storage can specify the AccessTokenType to be returned
+ tokenStorage, ok := creator.Storage().(JWTProfileTokenStorage)
+ if ok {
+ var err error
+ tokenType, err = tokenStorage.JWTProfileTokenType(ctx, tokenRequest)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ accessToken, _, validity, err := CreateAccessToken(ctx, tokenRequest, tokenType, creator, client, "")
+ if err != nil {
+ return nil, err
+ }
return &oidc.AccessTokenResponse{
AccessToken: accessToken,
TokenType: oidc.BearerToken,
- ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()),
+ ExpiresIn: uint64(validity.Seconds()),
}, nil
}
// ParseJWTProfileRequest has been renamed to ParseJWTProfileGrantRequest
//
-//deprecated: use ParseJWTProfileGrantRequest
+// deprecated: use ParseJWTProfileGrantRequest
func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) {
return ParseJWTProfileGrantRequest(r, decoder)
}
+
+type jwtProfileClient struct {
+ id string
+}
+
+func (j *jwtProfileClient) GetID() string {
+ return j.id
+}
+
+func (j *jwtProfileClient) ClockSkew() time.Duration {
+ return 0
+}
+
+func (j *jwtProfileClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
+ return func(scopes []string) []string {
+ return scopes
+ }
+}
+
+func (j *jwtProfileClient) GrantTypes() []oidc.GrantType {
+ return []oidc.GrantType{
+ oidc.GrantTypeBearer,
+ }
+}
diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go
index 7251eeb..148d2a4 100644
--- a/pkg/op/token_refresh.go
+++ b/pkg/op/token_refresh.go
@@ -6,9 +6,9 @@ import (
"net/http"
"time"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
- "github.com/zitadel/oidc/pkg/strings"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/strings"
)
type RefreshTokenRequest interface {
diff --git a/pkg/op/token_request.go b/pkg/op/token_request.go
index 6ccd489..b9e9805 100644
--- a/pkg/op/token_request.go
+++ b/pkg/op/token_request.go
@@ -5,15 +5,13 @@ import (
"net/http"
"net/url"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type Exchanger interface {
- Issuer() string
Storage() Storage
Decoder() httphelper.Decoder
- Signer() Signer
Crypto() Crypto
AuthMethodPostSupported() bool
AuthMethodPrivateKeyJWTSupported() bool
@@ -21,6 +19,9 @@ type Exchanger interface {
GrantTypeTokenExchangeSupported() bool
GrantTypeJWTAuthorizationSupported() bool
GrantTypeClientCredentialsSupported() bool
+ GrantTypeDeviceCodeSupported() bool
+ AccessTokenVerifier(context.Context) AccessTokenVerifier
+ IDTokenHintVerifier(context.Context) IDTokenHintVerifier
}
func tokenHandler(exchanger Exchanger) func(w http.ResponseWriter, r *http.Request) {
@@ -56,6 +57,11 @@ func Exchange(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
ClientCredentialsExchange(w, r, exchanger)
return
}
+ case string(oidc.GrantTypeDeviceCode):
+ if exchanger.GrantTypeDeviceCodeSupported() {
+ DeviceAccessToken(w, r, exchanger)
+ return
+ }
case "":
RequestError(w, r, oidc.ErrInvalidRequest().WithDescription("grant_type missing"))
return
@@ -122,7 +128,7 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C
// AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously
// registered public key (JWT Profile)
func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) {
- jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier())
+ jwtReq, err := VerifyJWTAssertion(ctx, clientAssertion, exchanger.JWTProfileVerifier(ctx))
if err != nil {
return nil, err
}
@@ -136,8 +142,8 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang
return client, nil
}
-// ValidateGrantType ensures that the requested grant_type is allowed by the Client
-func ValidateGrantType(client Client, grantType oidc.GrantType) bool {
+// ValidateGrantType ensures that the requested grant_type is allowed by the client
+func ValidateGrantType(client interface{ GrantTypes() []oidc.GrantType }, grantType oidc.GrantType) bool {
if client == nil {
return false
}
diff --git a/pkg/op/token_revocation.go b/pkg/op/token_revocation.go
index 9dd0295..58332c3 100644
--- a/pkg/op/token_revocation.go
+++ b/pkg/op/token_revocation.go
@@ -7,22 +7,22 @@ import (
"net/url"
"strings"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type Revoker interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
- AccessTokenVerifier() AccessTokenVerifier
+ AccessTokenVerifier(context.Context) AccessTokenVerifier
AuthMethodPrivateKeyJWTSupported() bool
AuthMethodPostSupported() bool
}
type RevokerJWTProfile interface {
Revoker
- JWTProfileVerifier() JWTProfileVerifier
+ JWTProfileVerifier(context.Context) JWTProfileVerifier
}
func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) {
@@ -39,8 +39,8 @@ func Revoke(w http.ResponseWriter, r *http.Request, revoker Revoker) {
}
var subject string
doDecrypt := true
- if canRefreshInfo, ok := revoker.Storage().(CanRefreshTokenInfo); ok && tokenTypeHint != "access_token" {
- userID, tokenID, err := canRefreshInfo.GetRefreshTokenInfo(r.Context(), clientID, token)
+ if tokenTypeHint != "access_token" {
+ userID, tokenID, err := revoker.Storage().GetRefreshTokenInfo(r.Context(), clientID, token)
if err != nil {
// An invalid refresh token means that we'll try other things (leaving doDecrypt==true)
if !errors.Is(err, ErrInvalidRefreshToken) {
@@ -87,7 +87,7 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token
if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() {
return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported")
}
- profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier())
+ profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, revokerJWTProfile.JWTProfileVerifier(r.Context()))
if err == nil {
return req.Token, req.TokenTypeHint, profile.Issuer, nil
}
@@ -151,9 +151,9 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use
}
return splitToken[0], splitToken[1], true
}
- accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier())
+ 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 4bd03e2..21a0af4 100644
--- a/pkg/op/userinfo.go
+++ b/pkg/op/userinfo.go
@@ -6,15 +6,15 @@ import (
"net/http"
"strings"
- httphelper "github.com/zitadel/oidc/pkg/http"
- "github.com/zitadel/oidc/pkg/oidc"
+ httphelper "github.com/zitadel/oidc/v2/pkg/http"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type UserinfoProvider interface {
Decoder() httphelper.Decoder
Crypto() Crypto
Storage() Storage
- AccessTokenVerifier() AccessTokenVerifier
+ AccessTokenVerifier(context.Context) AccessTokenVerifier
}
func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) {
@@ -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())
+ 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 1729c23..9a8b912 100644
--- a/pkg/op/verifier_access_token.go
+++ b/pkg/op/verifier_access_token.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type AccessTokenVerifier interface {
@@ -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 d36bbd8..d906075 100644
--- a/pkg/op/verifier_id_token_hint.go
+++ b/pkg/op/verifier_id_token_hint.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type IDTokenHintVerifier interface {
@@ -73,41 +73,41 @@ 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()
+// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+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)
+ })
+ }
+}
diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go
index 0215e84..4d83c59 100644
--- a/pkg/op/verifier_jwt_profile.go
+++ b/pkg/op/verifier_jwt_profile.go
@@ -8,7 +8,7 @@ import (
"gopkg.in/square/go-jose.v2"
- "github.com/zitadel/oidc/pkg/oidc"
+ "github.com/zitadel/oidc/v2/pkg/oidc"
)
type JWTProfileVerifier interface {
@@ -104,7 +104,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif
}
type jwtProfileKeyStorage interface {
- GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
+ GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
}
func SubjectIsIssuer(request *oidc.JWTTokenRequest) error {
@@ -122,7 +122,7 @@ type jwtProfileKeySet struct {
// VerifySignature implements oidc.KeySet by getting the public key from Storage implementation
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
keyID, _ := oidc.GetKeyIDAndAlg(jws)
- key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.clientID)
+ key, err := k.storage.GetKeyByIDAndClientID(ctx, keyID, k.clientID)
if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err)
}