feat(op): dynamic issuer depending on request / host (#278)

* feat(op): dynamic issuer depending on request / host

BREAKING CHANGE: The OpenID Provider package is now able to handle multiple issuers with a single storage implementation. The issuer will be selected from the host of the request and passed into the context, where every function can read it from if necessary. This results in some fundamental changes:
 - `Configuration` interface:
   - `Issuer() string` has been changed to `IssuerFromRequest(r *http.Request) string`
   - `Insecure() bool` has been added
 - OpenIDProvider interface and dependants:
   - `Issuer` has been removed from Config struct
   - `NewOpenIDProvider` now takes an additional parameter `issuer` and returns a pointer to the public/default implementation and not an OpenIDProvider interface:
     `NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opOpts ...Option) (OpenIDProvider, error)` changed to `NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error)`
   - therefore the parameter type Option changed to the public type as well: `Option func(o *Provider) error`
   - `AuthCallbackURL(o OpenIDProvider) func(string) string` has been changed to `AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string`
   - `IDTokenHintVerifier() IDTokenHintVerifier` (Authorizer, OpenIDProvider, SessionEnder interfaces), `AccessTokenVerifier() AccessTokenVerifier` (Introspector, OpenIDProvider, Revoker, UserinfoProvider interfaces) and `JWTProfileVerifier() JWTProfileVerifier` (IntrospectorJWTProfile, JWTAuthorizationGrantExchanger, OpenIDProvider, RevokerJWTProfile interfaces) now take a context.Context parameter `IDTokenHintVerifier(context.Context) IDTokenHintVerifier`, `AccessTokenVerifier(context.Context) AccessTokenVerifier` and `JWTProfileVerifier(context.Context) JWTProfileVerifier`
   - `OidcDevMode` (CAOS_OIDC_DEV) environment variable check has been removed, use `WithAllowInsecure()` Option
 - Signing: the signer is not kept in memory anymore, but created on request from the loaded key:
   - `Signer` interface and func `NewSigner` have been removed
   - `ReadySigner(s Signer) ProbesFn` has been removed
   - `CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfiguration` has been changed to `CreateDiscoveryConfig(r *http.Request, config Configuration, storage DiscoverStorage) *oidc.DiscoveryConfiguration`
   - `Storage` interface:
     - `GetSigningKey(context.Context, chan<- jose.SigningKey)` has been changed to `SigningKey(context.Context) (SigningKey, error)`
     - `KeySet(context.Context) ([]Key, error)` has been added
     - `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)`
   - `SigAlgorithms(s Signer) []string` has been changed to `SigAlgorithms(ctx context.Context, storage DiscoverStorage) []string`
   - KeyProvider interface: `GetKeySet(context.Context) (*jose.JSONWebKeySet, error)` has been changed to `KeySet(context.Context) ([]Key, error)`
   - `CreateIDToken`: the Signer parameter has been removed

* move example

* fix examples

* fix mocks

* update readme

* fix examples and update usage

* update go module version to v2

* build branch

* fix(module): rename caos to zitadel

* fix: add state in access token response (implicit flow)

* fix: encode auth response correctly (when using query in redirect uri)

* fix query param handling

* feat: add all optional claims of the introspection response

* fix: use default redirect uri when not passed

* fix: exchange cors library and add `X-Requested-With` to Access-Control-Request-Headers (#261)

* feat(op): add support for client credentials

* fix mocks and test

* feat: allow to specify token type of JWT Profile Grant

* document JWTProfileTokenStorage

* cleanup

* rp: fix integration test

test username needed to be suffixed by issuer domain

* chore(deps): bump golang.org/x/text from 0.5.0 to 0.6.0

Bumps [golang.org/x/text](https://github.com/golang/text) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* op: mock: cleanup commented code

* op: remove duplicate code

code duplication caused by merge conflict selections

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Tim Möhlmann 2023-02-09 18:10:22 +02:00 committed by GitHub
parent a34d7a1630
commit 1165d88c69
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2349 additions and 913 deletions

View file

@ -1,5 +1,8 @@
module.exports = { module.exports = {
branches: ["main"], branches: [
{name: "main"},
{name: "dynamic-issuer", prerelease: true},
],
plugins: [ plugins: [
"@semantic-release/commit-analyzer", "@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",

View file

@ -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/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/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/client/service demonstration of JWT Profile Authorization Grant /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
</pre> </pre>
## How To Use It ## How To Use It
@ -44,16 +44,27 @@ Check the `/example` folder where example code for different scenarios is locate
```bash ```bash
# start oidc op server # start oidc op server
# oidc discovery http://localhost:9998/.well-known/openid-configuration # 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) # 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/example/client/app
``` ```
- open http://localhost:9999/login in your browser - open http://localhost:9999/login in your browser
- you will be redirected to op server and the login UI - 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 - 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 ## Features
| | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials | | | Code Flow | Implicit Flow | Hybrid Flow | Discovery | PKCE | Token Exchange | mTLS | JWT Profile | Refresh Token | Client Credentials |

View file

@ -12,8 +12,8 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/oidc/pkg/client/rs" "github.com/zitadel/oidc/v2/pkg/client/rs"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (

View file

@ -11,9 +11,9 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/oidc/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
var ( var (

View file

@ -10,9 +10,9 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github" githubOAuth "golang.org/x/oauth2/github"
"github.com/zitadel/oidc/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
"github.com/zitadel/oidc/pkg/client/rp/cli" "github.com/zitadel/oidc/v2/pkg/client/rp/cli"
"github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/v2/pkg/http"
) )
var ( var (

View file

@ -13,7 +13,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/zitadel/oidc/pkg/client/profile" "github.com/zitadel/oidc/v2/pkg/client/profile"
) )
var client = http.DefaultClient var client = http.DefaultClient

View file

@ -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) /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 /github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/service demonstration of JWT Profile Authorization Grant /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 package example

View file

@ -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(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body style="display: flex; align-items: center; justify-content: center; height: 100vh;">
<form method="POST" action="/login/username" style="height: 200px; width: 200px;">
<input type="hidden" name="id" value="{{.ID}}">
<div>
<label for="username">Username:</label>
<input id="username" name="username" style="width: 100%">
</div>
<div>
<label for="password">Password:</label>
<input id="password" name="password" style="width: 100%">
</div>
<p style="color:red; min-height: 1rem;">{{.Error}}</p>
<button type="submit">Login</button>
</form>
</body>
</html>`)
)
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)
}

View file

@ -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(ctx, "/", 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
}

View file

@ -1,6 +1,7 @@
package exampleop package exampleop
import ( import (
"context"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
@ -44,10 +45,10 @@ var loginTmpl, _ = template.New("login").Parse(`
type login struct { type login struct {
authenticate authenticate authenticate authenticate
router *mux.Router 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{ l := &login{
authenticate: authenticate, authenticate: authenticate,
callback: callback, callback: callback,
@ -109,5 +110,5 @@ func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) {
renderLogin(w, id, err) renderLogin(w, id, err)
return return
} }
http.Redirect(w, r, l.callback(id), http.StatusFound) http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound)
} }

View file

@ -5,13 +5,12 @@ import (
"crypto/sha256" "crypto/sha256"
"log" "log"
"net/http" "net/http"
"os"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/oidc/example/server/storage" "github.com/zitadel/oidc/v2/example/server/storage"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
const ( const (
@ -35,9 +34,6 @@ type Storage interface {
// //
// Use one of the pre-made clients in storage/clients.go or register a new one. // 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 { 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")
// the OpenID Provider requires a 32-byte key for (token) encryption // the OpenID Provider requires a 32-byte key for (token) encryption
// be sure to create a proper crypto random key and manage it securely! // be sure to create a proper crypto random key and manage it securely!
key := sha256.Sum256([]byte("test")) key := sha256.Sum256([]byte("test"))
@ -81,7 +77,6 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
// it will enable all options (see descriptions) // 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(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
config := &op.Config{ config := &op.Config{
Issuer: issuer,
CryptoKey: key, CryptoKey: key,
// will be used if the end_session endpoint is called without a post_logout_redirect_uri // will be used if the end_session endpoint is called without a post_logout_redirect_uri
@ -105,7 +100,9 @@ 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 // this example has only static texts (in English), so we'll set the here accordingly
SupportedUILocales: []language.Tag{language.English}, SupportedUILocales: []language.Tag{language.English},
} }
handler, err := op.NewOpenIDProvider(ctx, config, storage, handler, err := op.NewOpenIDProvider(ctx, 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 // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
) )

View file

@ -2,23 +2,28 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"net/http" "net/http"
"github.com/zitadel/oidc/example/server/exampleop" "github.com/zitadel/oidc/v2/example/server/exampleop"
"github.com/zitadel/oidc/example/server/storage" "github.com/zitadel/oidc/v2/example/server/storage"
) )
func main() { func main() {
ctx := context.Background() 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 // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database // this might be the layer for accessing your database
// in this example it will be handled in-memory // 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, issuer, storage)
router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage)
server := &http.Server{ server := &http.Server{
Addr: ":" + port, Addr: ":" + port,

View file

@ -3,8 +3,8 @@ package storage
import ( import (
"time" "time"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
var ( var (

View file

@ -5,9 +5,8 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/pkg/oidc"
) )
const ( const (

View file

@ -12,8 +12,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant // serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
@ -45,9 +45,41 @@ type Storage struct {
} }
type signingKey struct { type signingKey struct {
ID string id string
Algorithm string algorithm jose.SignatureAlgorithm
Key *rsa.PrivateKey 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 { func NewStorage(userStore UserStore) *Storage {
@ -67,9 +99,9 @@ func NewStorage(userStore UserStore) *Storage {
}, },
}, },
signingKey: signingKey{ signingKey: signingKey{
ID: "id", id: uuid.NewString(),
Algorithm: "RS256", algorithm: jose.RS256,
Key: key, key: key,
}, },
} }
} }
@ -288,41 +320,29 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
return nil return nil
} }
// GetSigningKey implements the op.Storage interface // SigningKey implements the op.Storage interface
// it will be called when creating the OpenID Provider // 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 // 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 // you would obviously have a more complex implementation and store / retrieve the key from your database as well
// return &s.signingKey, nil
// 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,
},
}
} }
// 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, ... // 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, // as mentioned above, this example only has a single signing key without key rotation,
// so it will directly use its public key // 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 // 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 //and give both of them an expiration date, with the public key having a longer lifetime
return &jose.JSONWebKeySet{ return []op.Key{&publicKey{s.signingKey}}, nil
Keys: []jose.JSONWebKey{
{
KeyID: s.signingKey.ID,
Algorithm: s.signingKey.Algorithm,
Use: oidc.KeyUseSignature,
Key: &s.signingKey.Key.PublicKey,
},
},
}, nil
} }
// GetClientByClientID implements the op.Storage interface // GetClientByClientID implements the op.Storage interface

View file

@ -0,0 +1,260 @@
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)
}
// 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.UserInfoSetter, 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.UserInfoSetter, 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)
}
// GetKeyByIDAndUserID implements the op.Storage interface
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
func (s *multiStorage) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) {
storage, err := s.storageFromContext(ctx)
if err != nil {
return nil, err
}
return storage.GetKeyByIDAndUserID(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
}

View file

@ -2,6 +2,7 @@ package storage
import ( import (
"crypto/rsa" "crypto/rsa"
"strings"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -33,12 +34,13 @@ type userStore struct {
users map[string]*User users map[string]*User
} }
func NewUserStore() UserStore { func NewUserStore(issuer string) UserStore {
hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0]
return userStore{ return userStore{
users: map[string]*User{ users: map[string]*User{
"id1": { "id1": {
ID: "id1", ID: "id1",
Username: "test-user", Username: "test-user@" + hostname,
Password: "verysecure", Password: "verysecure",
FirstName: "Test", FirstName: "Test",
LastName: "User", LastName: "User",

5
go.mod
View file

@ -1,4 +1,4 @@
module github.com/zitadel/oidc module github.com/zitadel/oidc/v2
go 1.16 go 1.16
@ -15,9 +15,8 @@ require (
github.com/rs/cors v1.8.3 github.com/rs/cors v1.8.3
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
github.com/zitadel/logging v0.3.4
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/text v0.5.0 golang.org/x/text v0.6.0
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
) )

13
go.sum
View file

@ -131,13 +131,11 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 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 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -149,8 +147,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -252,7 +248,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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-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-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-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-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-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -272,7 +267,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
@ -285,8 +279,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -420,10 +414,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= 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/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.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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -14,9 +14,9 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
var Encoder = func() httphelper.Encoder { var Encoder = func() httphelper.Encoder {

View file

@ -5,8 +5,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// JWTProfileExchange handles the oauth2 jwt profile exchange // JWTProfileExchange handles the oauth2 jwt profile exchange

View file

@ -7,8 +7,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// jwtProfileTokenSource implement the oauth2.TokenSource // jwtProfileTokenSource implement the oauth2.TokenSource

View file

@ -4,9 +4,9 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/zitadel/oidc/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (

View file

@ -1,7 +1,7 @@
package rp package rp
import ( import (
"github.com/zitadel/oidc/pkg/oidc/grants/tokenexchange" "github.com/zitadel/oidc/v2/pkg/oidc/grants/tokenexchange"
) )
// DelegationTokenRequest is an implementation of TokenExchangeRequest // DelegationTokenRequest is an implementation of TokenExchangeRequest

View file

@ -15,28 +15,27 @@ import (
"testing" "testing"
"time" "time"
"github.com/zitadel/oidc/example/server/exampleop"
"github.com/zitadel/oidc/example/server/storage"
"github.com/jeremija/gosubmit" "github.com/jeremija/gosubmit"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zitadel/oidc/pkg/client/rp" "github.com/zitadel/oidc/v2/example/server/exampleop"
httphelper "github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/v2/example/server/storage"
"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"
) )
func TestRelyingPartySession(t *testing.T) { func TestRelyingPartySession(t *testing.T) {
t.Log("------- start example OP ------") t.Log("------- start example OP ------")
ctx := context.Background() ctx := context.Background()
exampleStorage := storage.NewStorage(storage.NewUserStore()) targetURL := "http://local-site"
exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL))
var dh deferredHandler var dh deferredHandler
opServer := httptest.NewServer(&dh) opServer := httptest.NewServer(&dh)
defer opServer.Close() defer opServer.Close()
t.Logf("auth server at %s", opServer.URL) t.Logf("auth server at %s", opServer.URL)
dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage) dh.Handler = exampleop.SetupServer(ctx, opServer.URL, exampleStorage)
targetURL := "http://local-site"
localURL, err := url.Parse(targetURL + "/login?requestID=1234") localURL, err := url.Parse(targetURL + "/login?requestID=1234")
require.NoError(t, err, "local url") require.NoError(t, err, "local url")
@ -109,7 +108,7 @@ func TestRelyingPartySession(t *testing.T) {
t.Log("------- post to login form, get redirect to OP ------") t.Log("------- post to login form, get redirect to OP ------")
postLoginRedirectURL := fillForm(t, "fill login form", httpClient, form, loginPageURL, 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")) gosubmit.Set("password", "verysecure"))
t.Logf("Get redirect from %s", postLoginRedirectURL) t.Logf("Get redirect from %s", postLoginRedirectURL)

View file

@ -9,8 +9,8 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet { func NewRemoteKeySet(client *http.Client, jwksURL string, opts ...func(*remoteKeySet)) oidc.KeySet {

View file

@ -10,7 +10,7 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// MockVerifier is a mock of Verifier interface // MockVerifier is a mock of Verifier interface

View file

@ -14,9 +14,9 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (

View file

@ -5,7 +5,7 @@ import (
"golang.org/x/oauth2" "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` // TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange`

View file

@ -6,7 +6,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type IDTokenVerifier interface { type IDTokenVerifier interface {

View file

@ -6,9 +6,9 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/zitadel/oidc/pkg/client" "github.com/zitadel/oidc/v2/pkg/client"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type ResourceServer interface { type ResourceServer interface {

View file

@ -3,7 +3,7 @@ package oidc
import ( import (
"crypto/sha256" "crypto/sha256"
"github.com/zitadel/oidc/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
) )
const ( const (

View file

@ -9,8 +9,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
"github.com/zitadel/oidc/pkg/http" "github.com/zitadel/oidc/v2/pkg/http"
) )
const ( const (

View file

@ -218,4 +218,6 @@ type ClientCredentialsRequest struct {
Scope SpaceDelimitedArray `schema:"scope"` Scope SpaceDelimitedArray `schema:"scope"`
ClientID string `schema:"client_id"` ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"` ClientSecret string `schema:"client_secret"`
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
} }

View file

@ -12,7 +12,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
str "github.com/zitadel/oidc/pkg/strings" str "github.com/zitadel/oidc/v2/pkg/strings"
) )
type Claims interface { type Claims interface {

View file

@ -11,9 +11,9 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
str "github.com/zitadel/oidc/pkg/strings" str "github.com/zitadel/oidc/v2/pkg/strings"
) )
type AuthRequest interface { type AuthRequest interface {
@ -38,10 +38,8 @@ type Authorizer interface {
Storage() Storage Storage() Storage
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Encoder() httphelper.Encoder Encoder() httphelper.Encoder
Signer() Signer IDTokenHintVerifier(context.Context) IDTokenHintVerifier
IDTokenHintVerifier() IDTokenHintVerifier
Crypto() Crypto Crypto() Crypto
Issuer() string
RequestObjectSupported() bool RequestObjectSupported() bool
} }
@ -72,8 +70,9 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, err, authorizer.Encoder()) AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return return
} }
ctx := r.Context()
if authReq.RequestParam != "" && authorizer.RequestObjectSupported() { 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 { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder()) AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return return
@ -91,7 +90,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
if validater, ok := authorizer.(AuthorizeValidator); ok { if validater, ok := authorizer.(AuthorizeValidator); ok {
validation = validater.ValidateAuthRequest 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 { if err != nil {
AuthRequestError(w, r, authReq, err, authorizer.Encoder()) AuthRequestError(w, r, authReq, err, authorizer.Encoder())
return return
@ -100,12 +99,12 @@ func Authorize(w http.ResponseWriter, r *http.Request, authorizer Authorizer) {
AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder()) AuthRequestError(w, r, authReq, oidc.ErrRequestNotSupported(), authorizer.Encoder())
return return
} }
req, err := authorizer.Storage().CreateAuthRequest(r.Context(), authReq, userID) req, err := authorizer.Storage().CreateAuthRequest(ctx, authReq, userID)
if err != nil { if err != nil {
AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder()) AuthRequestError(w, r, authReq, oidc.DefaultToServerError(err, "unable to save auth request"), authorizer.Encoder())
return return
} }
client, err := authorizer.Storage().GetClientByClientID(r.Context(), req.GetClientID()) client, err := authorizer.Storage().GetClientByClientID(ctx, req.GetClientID())
if err != nil { if err != nil {
AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder()) AuthRequestError(w, r, req, oidc.DefaultToServerError(err, "unable to retrieve client by id"), authorizer.Encoder())
return return

View file

@ -13,10 +13,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock" "github.com/zitadel/oidc/v2/pkg/op/mock"
) )
// //

View file

@ -3,7 +3,7 @@ package op
import ( import (
"time" "time"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
//go:generate go get github.com/dmarkham/enumer //go:generate go get github.com/dmarkham/enumer

View file

@ -2,20 +2,24 @@ package op
import ( import (
"errors" "errors"
"net/http"
"net/url" "net/url"
"os" "strings"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
const ( var (
OidcDevMode = "ZITADEL_OIDC_DEV" ErrInvalidIssuerPath = errors.New("no fragments or query allowed for issuer")
// deprecated: use OidcDevMode (ZITADEL_OIDC_DEV=true) ErrInvalidIssuerNoIssuer = errors.New("missing issuer")
devMode = "CAOS_OIDC_DEV" 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 { type Configuration interface {
Issuer() string IssuerFromRequest(r *http.Request) string
Insecure() bool
AuthorizationEndpoint() Endpoint AuthorizationEndpoint() Endpoint
TokenEndpoint() Endpoint TokenEndpoint() Endpoint
IntrospectionEndpoint() Endpoint IntrospectionEndpoint() Endpoint
@ -42,36 +46,74 @@ type Configuration interface {
SupportedUILocales() []language.Tag SupportedUILocales() []language.Tag
} }
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 == "" { if issuer == "" {
return errors.New("missing issuer") return ErrInvalidIssuerNoIssuer
} }
u, err := url.Parse(issuer) u, err := url.Parse(issuer)
if err != nil { if err != nil {
return errors.New("invalid url for issuer") return ErrInvalidIssuerURL
} }
if u.Host == "" { if u.Host == "" {
return errors.New("host for issuer missing") return ErrInvalidIssuerMissingHost
} }
if u.Scheme != "https" { if u.Scheme != "https" {
if !devLocalAllowed(u) { if !devLocalAllowed(u, allowInsecure) {
return errors.New("scheme for issuer must be `https`") return ErrInvalidIssuerHTTPS
} }
} }
if u.Fragment != "" || len(u.Query()) > 0 { return ValidateIssuerPath(u)
return errors.New("no fragments or query allowed for issuer") }
func ValidateIssuerPath(issuer *url.URL) error {
if issuer.Fragment != "" || len(issuer.Query()) > 0 {
return ErrInvalidIssuerPath
} }
return nil return nil
} }
func devLocalAllowed(url *url.URL) bool { func devLocalAllowed(url *url.URL, allowInsecure bool) bool {
_, b := os.LookupEnv(OidcDevMode) if !allowInsecure {
if !b { return false
// check the old / current env var as well
_, b = os.LookupEnv(devMode)
if !b {
return b
}
} }
return url.Scheme == "http" 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
}

View file

@ -1,13 +1,17 @@
package op package op
import ( import (
"os" "net/http/httptest"
"net/url"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestValidateIssuer(t *testing.T) { func TestValidateIssuer(t *testing.T) {
type args struct { type args struct {
issuer string issuer string
allowInsecure bool
} }
tests := []struct { tests := []struct {
name string name string
@ -16,65 +20,97 @@ func TestValidateIssuer(t *testing.T) {
}{ }{
{ {
"missing issuer fails", "missing issuer fails",
args{""}, args{
issuer: "",
},
true, true,
}, },
{ {
"invalid url for issuer fails", "invalid url for issuer fails",
args{":issuer"}, args{
true, issuer: ":issuer",
}, },
{
"invalid url for issuer fails",
args{":issuer"},
true, true,
}, },
{ {
"host for issuer missing fails", "host for issuer missing fails",
args{"https:///issuer"}, args{
true, issuer: "https:///issuer",
}, },
{
"host for not https fails",
args{"http://issuer.com"},
true, true,
}, },
{ {
"host with fragment fails", "host with fragment fails",
args{"https://issuer.com/#issuer"}, args{
issuer: "https://issuer.com/#issuer",
},
true, true,
}, },
{ {
"host with query fails", "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, true,
}, },
{ {
"host with https ok", "host with https ok",
args{"https://issuer.com"}, args{
issuer: "https://issuer.com",
},
false, false,
}, },
{ {
"localhost with http fails", "custom scheme fails",
args{"http://localhost:9999"}, 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, true,
}, },
} }
// ensure env is not set
//nolint:errcheck
os.Unsetenv(OidcDevMode)
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, tt.wantErr)
} }
}) })
} }
} }
func TestValidateIssuerDevLocalAllowed(t *testing.T) { func TestValidateIssuerPath(t *testing.T) {
type args struct { type args struct {
issuer string issuerPath *url.URL
} }
tests := []struct { tests := []struct {
name string name string
@ -82,17 +118,217 @@ func TestValidateIssuerDevLocalAllowed(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
"localhost with http with dev ok", "empty ok",
args{"http://localhost:9999"}, args{func() *url.URL {
u, _ := url.Parse("")
return u
}()},
false, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if err := ValidateIssuer(tt.args.issuer); (err != nil) != tt.wantErr { if err := ValidateIssuerPath(tt.args.issuerPath); (err != nil) != tt.wantErr {
t.Errorf("ValidateIssuer() error = %v, wantErr %v", err, 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)
} }
}) })
} }

49
pkg/op/context.go Normal file
View file

@ -0,0 +1,49 @@
package op
import (
"context"
"net/http"
)
type key int
var (
issuer 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(issuer).(string)
return ctxIssuer
}
func (i *IssuerInterceptor) setIssuerCtx(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := context.WithValue(r.Context(), issuer, i.issuerFromRequest(r))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}

76
pkg/op/context_test.go Normal file
View file

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

View file

@ -1,7 +1,7 @@
package op package op
import ( import (
"github.com/zitadel/oidc/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
) )
type Crypto interface { type Crypto interface {

View file

@ -1,49 +1,17 @@
package op package op
import ( import (
"context"
"net/http" "net/http"
httphelper "github.com/zitadel/oidc/pkg/http" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
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) { type DiscoverStorage interface {
return func(w http.ResponseWriter, r *http.Request) { SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error)
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(),
}
} }
var DefaultSupportedScopes = []string{ var DefaultSupportedScopes = []string{
@ -55,6 +23,46 @@ var DefaultSupportedScopes = []string{
oidc.ScopeOfflineAccess, 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),
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 { func Scopes(c Configuration) []string {
return DefaultSupportedScopes // TODO: config return DefaultSupportedScopes // TODO: config
} }
@ -87,6 +95,88 @@ func GrantTypes(c Configuration) []oidc.GrantType {
return grantTypes 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 { func SupportedClaims(c Configuration) []string {
return []string{ // TODO: config return []string{ // TODO: config
"sub", "sub",
@ -116,59 +206,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 { func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() { if c.CodeMethodS256Supported() {
@ -176,24 +213,3 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
} }
return codeMethods 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()
}

View file

@ -1,18 +1,19 @@
package op_test package op_test
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock" "github.com/zitadel/oidc/v2/pkg/op/mock"
) )
func TestDiscover(t *testing.T) { func TestDiscover(t *testing.T) {
@ -47,8 +48,9 @@ func TestDiscover(t *testing.T) {
func TestCreateDiscoveryConfig(t *testing.T) { func TestCreateDiscoveryConfig(t *testing.T) {
type args struct { type args struct {
request *http.Request
c op.Configuration c op.Configuration
s op.Signer s op.DiscoverStorage
} }
tests := []struct { tests := []struct {
name string name string
@ -59,9 +61,8 @@ func TestCreateDiscoveryConfig(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.CreateDiscoveryConfig(tt.args.c, tt.args.s); !reflect.DeepEqual(got, tt.want) { got := op.CreateDiscoveryConfig(tt.args.request, tt.args.c, tt.args.s)
t.Errorf("CreateDiscoveryConfig() = %v, want %v", got, tt.want) assert.Equal(t, tt.want, got)
}
}) })
} }
} }
@ -83,9 +84,8 @@ func Test_scopes(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.Scopes(tt.args.c); !reflect.DeepEqual(got, tt.want) { got := op.Scopes(tt.args.c)
t.Errorf("scopes() = %v, want %v", got, tt.want) assert.Equal(t, tt.want, got)
}
}) })
} }
} }
@ -99,13 +99,16 @@ func Test_ResponseTypes(t *testing.T) {
args args args args
want []string want []string
}{ }{
// TODO: Add test cases. {
"code and implicit flow",
args{},
[]string{"code", "id_token", "id_token token"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.ResponseTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) { got := op.ResponseTypes(tt.args.c)
t.Errorf("responseTypes() = %v, want %v", got, tt.want) assert.Equal(t, tt.want, got)
}
}) })
} }
} }
@ -117,63 +120,51 @@ func Test_GrantTypes(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
want []string want []oidc.GrantType
}{
// 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
}{ }{
{ {
"", "code and implicit flow",
args{func() op.Signer { args{
m.EXPECT().SignatureAlgorithm().Return(jose.RS256) func() op.Configuration {
return m c := mock.NewMockConfiguration(gomock.NewController(t))
}()}, c.EXPECT().GrantTypeRefreshTokenSupported().Return(false)
[]string{"RS256"}, c.EXPECT().GrantTypeTokenExchangeSupported().Return(false)
c.EXPECT().GrantTypeJWTAuthorizationSupported().Return(false)
c.EXPECT().GrantTypeClientCredentialsSupported().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)
return c
}(),
},
[]oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
oidc.GrantTypeRefreshToken,
oidc.GrantTypeClientCredentials,
oidc.GrantTypeTokenExchange,
oidc.GrantTypeBearer,
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.SigAlgorithms(tt.args.s); !reflect.DeepEqual(got, tt.want) { got := op.GrantTypes(tt.args.c)
t.Errorf("sigAlgorithms() = %v, want %v", got, tt.want) assert.Equal(t, tt.want, got)
}
}) })
} }
} }
@ -195,9 +186,80 @@ func Test_SubjectTypes(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.SubjectTypes(tt.args.c); !reflect.DeepEqual(got, tt.want) { got := op.SubjectTypes(tt.args.c)
t.Errorf("subjectTypes() = %v, want %v", got, tt.want) 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 +306,311 @@ func Test_AuthMethodsTokenEndpoint(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := op.AuthMethodsTokenEndpoint(tt.args.c); !reflect.DeepEqual(got, tt.want) { got := op.AuthMethodsTokenEndpoint(tt.args.c)
t.Errorf("authMethods() = %v, want %v", got, tt.want) 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)
}) })
} }
} }

View file

@ -3,7 +3,7 @@ package op_test
import ( import (
"testing" "testing"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
func TestEndpoint_Path(t *testing.T) { func TestEndpoint_Path(t *testing.T) {

View file

@ -3,8 +3,8 @@ package op
import ( import (
"net/http" "net/http"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type ErrAuthRequest interface { type ErrAuthRequest interface {

View file

@ -6,11 +6,11 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
) )
type KeyProvider interface { type KeyProvider interface {
GetKeySet(context.Context) (*jose.JSONWebKeySet, error) KeySet(context.Context) ([]Key, error)
} }
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) { 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) { 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 { if err != nil {
httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError) httphelper.MarshalJSONWithStatus(w, err, http.StatusInternalServerError)
return 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}
} }

View file

@ -11,9 +11,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/oidc/pkg/op/mock" "github.com/zitadel/oidc/v2/pkg/op/mock"
) )
func TestKeys(t *testing.T) { func TestKeys(t *testing.T) {
@ -35,7 +35,7 @@ func TestKeys(t *testing.T) {
args: args{ args: args{
k: func() op.KeyProvider { k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t)) 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 return m
}(), }(),
}, },
@ -51,39 +51,39 @@ func TestKeys(t *testing.T) {
args: args{ args: args{
k: func() op.KeyProvider { k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t)) m := mock.NewMockKeyProvider(gomock.NewController(t))
m.EXPECT().GetKeySet(gomock.Any()).Return(nil, nil) m.EXPECT().KeySet(gomock.Any()).Return(nil, nil)
return m return m
}(), }(),
}, },
res: res{ res: res{
statusCode: http.StatusOK, statusCode: http.StatusOK,
contentType: "application/json", contentType: "application/json",
body: `{"keys":[]}
`,
}, },
}, },
{ {
name: "list", name: "list",
args: args{ args: args{
k: func() op.KeyProvider { k: func() op.KeyProvider {
m := mock.NewMockKeyProvider(gomock.NewController(t)) ctrl := gomock.NewController(t)
m.EXPECT().GetKeySet(gomock.Any()).Return( m := mock.NewMockKeyProvider(ctrl)
&jose.JSONWebKeySet{Keys: []jose.JSONWebKey{ k := mock.NewMockKey(ctrl)
{ k.EXPECT().Key().Return(&rsa.PublicKey{
Key: &rsa.PublicKey{
N: big.NewInt(1), N: big.NewInt(1),
E: 1, E: 1,
}, })
KeyID: "id", k.EXPECT().ID().Return("id")
}, k.EXPECT().Algorithm().Return(jose.RS256)
}}, k.EXPECT().Use().Return("sig")
nil, m.EXPECT().KeySet(gomock.Any()).Return([]op.Key{k}, nil)
)
return m return m
}(), }(),
}, },
res: res{ res: res{
statusCode: http.StatusOK, statusCode: http.StatusOK,
contentType: "application/json", 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"}]}
`, `,
}, },
}, },

View file

@ -1,15 +1,16 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
context "context"
reflect "reflect" reflect "reflect"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
http "github.com/zitadel/oidc/pkg/http" http "github.com/zitadel/oidc/v2/pkg/http"
op "github.com/zitadel/oidc/pkg/op" op "github.com/zitadel/oidc/v2/pkg/op"
) )
// MockAuthorizer is a mock of Authorizer interface. // MockAuthorizer is a mock of Authorizer interface.
@ -78,31 +79,17 @@ func (mr *MockAuthorizerMockRecorder) Encoder() *gomock.Call {
} }
// IDTokenHintVerifier mocks base method. // IDTokenHintVerifier mocks base method.
func (m *MockAuthorizer) IDTokenHintVerifier() op.IDTokenHintVerifier { func (m *MockAuthorizer) IDTokenHintVerifier(arg0 context.Context) op.IDTokenHintVerifier {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IDTokenHintVerifier") ret := m.ctrl.Call(m, "IDTokenHintVerifier", arg0)
ret0, _ := ret[0].(op.IDTokenHintVerifier) ret0, _ := ret[0].(op.IDTokenHintVerifier)
return ret0 return ret0
} }
// IDTokenHintVerifier indicates an expected call of IDTokenHintVerifier. // 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() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IDTokenHintVerifier", reflect.TypeOf((*MockAuthorizer)(nil).IDTokenHintVerifier), arg0)
}
// 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))
} }
// RequestObjectSupported mocks base method. // 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)) 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. // Storage mocks base method.
func (m *MockAuthorizer) Storage() op.Storage { func (m *MockAuthorizer) Storage() op.Storage {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View file

@ -8,8 +8,8 @@ import (
"github.com/gorilla/schema" "github.com/gorilla/schema"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
func NewAuthorizer(t *testing.T) op.Authorizer { func NewAuthorizer(t *testing.T) op.Authorizer {
@ -20,23 +20,13 @@ func NewAuthorizerExpectValid(t *testing.T, wantErr bool) op.Authorizer {
m := NewAuthorizer(t) m := NewAuthorizer(t)
ExpectDecoder(m) ExpectDecoder(m)
ExpectEncoder(m) ExpectEncoder(m)
ExpectSigner(m, t) //ExpectSigner(m, t)
ExpectStorage(m, t) ExpectStorage(m, t)
ExpectVerifier(m, t) ExpectVerifier(m, t)
// ExpectErrorHandler(m, t, wantErr) // ExpectErrorHandler(m, t, wantErr)
return m 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) { func ExpectDecoder(a op.Authorizer) {
mockA := a.(*MockAuthorizer) mockA := a.(*MockAuthorizer)
mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder()) mockA.EXPECT().Decoder().AnyTimes().Return(schema.NewDecoder())
@ -47,17 +37,18 @@ func ExpectEncoder(a op.Authorizer) {
mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder()) mockA.EXPECT().Encoder().AnyTimes().Return(schema.NewEncoder())
} }
func ExpectSigner(a op.Authorizer, t *testing.T) { //
mockA := a.(*MockAuthorizer) //func ExpectSigner(a op.Authorizer, t *testing.T) {
mockA.EXPECT().Signer().DoAndReturn( // mockA := a.(*MockAuthorizer)
func() op.Signer { // mockA.EXPECT().Signer().DoAndReturn(
return &Sig{} // func() op.Signer {
}) // return &Sig{}
} // })
//}
func ExpectVerifier(a op.Authorizer, t *testing.T) { func ExpectVerifier(a op.Authorizer, t *testing.T) {
mockA := a.(*MockAuthorizer) mockA := a.(*MockAuthorizer)
mockA.EXPECT().IDTokenHintVerifier().DoAndReturn( mockA.EXPECT().IDTokenHintVerifier(gomock.Any()).DoAndReturn(
func() op.IDTokenHintVerifier { func() op.IDTokenHintVerifier {
return op.NewIDTokenHintVerifier("", nil) return op.NewIDTokenHintVerifier("", nil)
}) })

View file

@ -5,8 +5,8 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/op" "github.com/zitadel/oidc/v2/pkg/op"
) )
func NewClient(t *testing.T) op.Client { func NewClient(t *testing.T) op.Client {

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
@ -9,8 +9,8 @@ import (
time "time" time "time"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/pkg/oidc" oidc "github.com/zitadel/oidc/v2/pkg/oidc"
op "github.com/zitadel/oidc/pkg/op" op "github.com/zitadel/oidc/v2/pkg/op"
) )
// MockClient is a mock of Client interface. // MockClient is a mock of Client interface.

View file

@ -1,14 +1,15 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
http "net/http"
reflect "reflect" reflect "reflect"
gomock "github.com/golang/mock/gomock" 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" language "golang.org/x/text/language"
) )
@ -161,6 +162,20 @@ func (mr *MockConfigurationMockRecorder) GrantTypeTokenExchangeSupported() *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrantTypeTokenExchangeSupported", reflect.TypeOf((*MockConfiguration)(nil).GrantTypeTokenExchangeSupported)) 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. // IntrospectionAuthMethodPrivateKeyJWTSupported mocks base method.
func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { func (m *MockConfiguration) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -203,18 +218,18 @@ func (mr *MockConfigurationMockRecorder) IntrospectionEndpointSigningAlgorithmsS
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntrospectionEndpointSigningAlgorithmsSupported", reflect.TypeOf((*MockConfiguration)(nil).IntrospectionEndpointSigningAlgorithmsSupported))
} }
// Issuer mocks base method. // IssuerFromRequest mocks base method.
func (m *MockConfiguration) Issuer() string { func (m *MockConfiguration) IssuerFromRequest(arg0 *http.Request) string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Issuer") ret := m.ctrl.Call(m, "IssuerFromRequest", arg0)
ret0, _ := ret[0].(string) ret0, _ := ret[0].(string)
return ret0 return ret0
} }
// Issuer indicates an expected call of Issuer. // IssuerFromRequest indicates an expected call of IssuerFromRequest.
func (mr *MockConfigurationMockRecorder) Issuer() *gomock.Call { func (mr *MockConfigurationMockRecorder) IssuerFromRequest(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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. // KeysEndpoint mocks base method.

View file

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

View file

@ -1,8 +1,9 @@
package mock package mock
//go:generate mockgen -package mock -destination ./storage.mock.go github.com/zitadel/oidc/pkg/op Storage //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/pkg/op Authorizer //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/pkg/op Client //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/pkg/op Configuration //go:generate mockgen -package mock -destination ./configuration.mock.go github.com/zitadel/oidc/v2/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 ./discovery.mock.go github.com/zitadel/oidc/v2/pkg/op DiscoverStorage
//go:generate mockgen -package mock -destination ./key.mock.go github.com/zitadel/oidc/pkg/op KeyProvider //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

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
@ -9,7 +9,7 @@ import (
reflect "reflect" reflect "reflect"
gomock "github.com/golang/mock/gomock" 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. // MockKeyProvider is a mock of KeyProvider interface.
@ -35,17 +35,17 @@ func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder {
return m.recorder return m.recorder
} }
// GetKeySet mocks base method. // KeySet mocks base method.
func (m *MockKeyProvider) GetKeySet(arg0 context.Context) (*jose.JSONWebKeySet, error) { func (m *MockKeyProvider) KeySet(arg0 context.Context) ([]op.Key, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetKeySet", arg0) ret := m.ctrl.Call(m, "KeySet", arg0)
ret0, _ := ret[0].(*jose.JSONWebKeySet) ret0, _ := ret[0].([]op.Key)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetKeySet indicates an expected call of GetKeySet. // KeySet indicates an expected call of KeySet.
func (mr *MockKeyProviderMockRecorder) GetKeySet(arg0 interface{}) *gomock.Call { func (mr *MockKeyProviderMockRecorder) KeySet(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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)
} }

View file

@ -1,56 +1,69 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
context "context"
reflect "reflect" reflect "reflect"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )
// MockSigner is a mock of Signer interface. // MockSigningKey is a mock of SigningKey interface.
type MockSigner struct { type MockSigningKey struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockSignerMockRecorder recorder *MockSigningKeyMockRecorder
} }
// MockSignerMockRecorder is the mock recorder for MockSigner. // MockSigningKeyMockRecorder is the mock recorder for MockSigningKey.
type MockSignerMockRecorder struct { type MockSigningKeyMockRecorder struct {
mock *MockSigner mock *MockSigningKey
} }
// NewMockSigner creates a new mock instance. // NewMockSigningKey creates a new mock instance.
func NewMockSigner(ctrl *gomock.Controller) *MockSigner { func NewMockSigningKey(ctrl *gomock.Controller) *MockSigningKey {
mock := &MockSigner{ctrl: ctrl} mock := &MockSigningKey{ctrl: ctrl}
mock.recorder = &MockSignerMockRecorder{mock} mock.recorder = &MockSigningKeyMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use. // 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 return m.recorder
} }
// Health mocks base method. // ID mocks base method.
func (m *MockSigner) Health(arg0 context.Context) error { func (m *MockSigningKey) ID() string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health", arg0) ret := m.ctrl.Call(m, "ID")
ret0, _ := ret[0].(error) ret0, _ := ret[0].(string)
return ret0 return ret0
} }
// Health indicates an expected call of Health. // ID indicates an expected call of ID.
func (mr *MockSignerMockRecorder) Health(arg0 interface{}) *gomock.Call { func (mr *MockSigningKeyMockRecorder) ID() *gomock.Call {
mr.mock.ctrl.T.Helper() 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. // SignatureAlgorithm mocks base method.
func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm { func (m *MockSigningKey) SignatureAlgorithm() jose.SignatureAlgorithm {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignatureAlgorithm") ret := m.ctrl.Call(m, "SignatureAlgorithm")
ret0, _ := ret[0].(jose.SignatureAlgorithm) ret0, _ := ret[0].(jose.SignatureAlgorithm)
@ -58,21 +71,86 @@ func (m *MockSigner) SignatureAlgorithm() jose.SignatureAlgorithm {
} }
// SignatureAlgorithm indicates an expected call of 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() 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. // MockKey is a mock of Key interface.
func (m *MockSigner) Signer() jose.Signer { 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Signer") ret := m.ctrl.Call(m, "Algorithm")
ret0, _ := ret[0].(jose.Signer) ret0, _ := ret[0].(jose.SignatureAlgorithm)
return ret0 return ret0
} }
// Signer indicates an expected call of Signer. // Algorithm indicates an expected call of Algorithm.
func (mr *MockSignerMockRecorder) Signer() *gomock.Call { func (mr *MockKeyMockRecorder) Algorithm() *gomock.Call {
mr.mock.ctrl.T.Helper() 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))
} }

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
@ -10,8 +10,8 @@ import (
time "time" time "time"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
oidc "github.com/zitadel/oidc/pkg/oidc" oidc "github.com/zitadel/oidc/v2/pkg/oidc"
op "github.com/zitadel/oidc/pkg/op" op "github.com/zitadel/oidc/v2/pkg/op"
jose "gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
) )
@ -174,21 +174,6 @@ func (mr *MockStorageMockRecorder) GetKeyByIDAndUserID(arg0, arg1, arg2 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyByIDAndUserID", reflect.TypeOf((*MockStorage)(nil).GetKeyByIDAndUserID), arg0, arg1, arg2) 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)
}
// GetPrivateClaimsFromScopes mocks base method. // GetPrivateClaimsFromScopes mocks base method.
func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (map[string]interface{}, error) { func (m *MockStorage) GetPrivateClaimsFromScopes(arg0 context.Context, arg1, arg2 string, arg3 []string) (map[string]interface{}, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -204,18 +189,6 @@ func (mr *MockStorageMockRecorder) GetPrivateClaimsFromScopes(arg0, arg1, arg2,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrivateClaimsFromScopes", reflect.TypeOf((*MockStorage)(nil).GetPrivateClaimsFromScopes), arg0, arg1, arg2, arg3) 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) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "GetSigningKey", arg0, arg1)
}
// GetSigningKey indicates an expected call of GetSigningKey.
func (mr *MockStorageMockRecorder) GetSigningKey(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigningKey", reflect.TypeOf((*MockStorage)(nil).GetSigningKey), arg0, arg1)
}
// Health mocks base method. // Health mocks base method.
func (m *MockStorage) Health(arg0 context.Context) error { func (m *MockStorage) Health(arg0 context.Context) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -230,6 +203,21 @@ func (mr *MockStorageMockRecorder) Health(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockStorage)(nil).Health), arg0) 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. // RevokeToken mocks base method.
func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error { func (m *MockStorage) RevokeToken(arg0 context.Context, arg1, arg2, arg3 string) *oidc.Error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -300,6 +288,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) 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. // TerminateSession mocks base method.
func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error { func (m *MockStorage) TerminateSession(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View file

@ -6,13 +6,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/zitadel/oidc/pkg/oidc"
"gopkg.in/square/go-jose.v2"
"github.com/golang/mock/gomock" "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 { 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 { func NewMockStorageSigningKeyInvalid(t *testing.T) op.Storage {
m := NewStorage(t) m := NewStorage(t)
ExpectSigningKeyInvalid(m) //ExpectSigningKeyInvalid(m)
return m return m
} }
func NewMockStorageSigningKey(t *testing.T) op.Storage { func NewMockStorageSigningKey(t *testing.T) op.Storage {
m := NewStorage(t) m := NewStorage(t)
ExpectSigningKey(m) //ExpectSigningKey(m)
return 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 { type ConfClient struct {
id string id string
appType op.ApplicationType appType op.ApplicationType

View file

@ -12,8 +12,8 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
const ( const (
@ -29,7 +29,8 @@ const (
defaultKeysEndpoint = "keys" defaultKeysEndpoint = "keys"
) )
var DefaultEndpoints = &endpoints{ var (
DefaultEndpoints = &endpoints{
Authorization: NewEndpoint(defaultAuthorizationEndpoint), Authorization: NewEndpoint(defaultAuthorizationEndpoint),
Token: NewEndpoint(defaultTokenEndpoint), Token: NewEndpoint(defaultTokenEndpoint),
Introspection: NewEndpoint(defaultIntrospectEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint),
@ -39,23 +40,7 @@ var DefaultEndpoints = &endpoints{
JwksURI: NewEndpoint(defaultKeysEndpoint), JwksURI: NewEndpoint(defaultKeysEndpoint),
} }
type OpenIDProvider interface { defaultCORSOptions = cors.Options{
Configuration
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
IDTokenHintVerifier() IDTokenHintVerifier
AccessTokenVerifier() 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, AllowCredentials: true,
AllowedHeaders: []string{ AllowedHeaders: []string{
"Origin", "Origin",
@ -78,29 +63,45 @@ var defaultCORSOptions = cors.Options{
return true return true
}, },
} }
)
type OpenIDProvider interface {
Configuration
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
IDTokenHintVerifier(context.Context) IDTokenHintVerifier
AccessTokenVerifier(context.Context) AccessTokenVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Probes() []ProbesFn
HttpHandler() http.Handler
}
type HttpInterceptor func(http.Handler) http.Handler
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router { func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router {
intercept := buildInterceptor(interceptors...)
router := mux.NewRouter() router := mux.NewRouter()
router.Use(cors.New(defaultCORSOptions).Handler) router.Use(cors.New(defaultCORSOptions).Handler)
router.Use(intercept(o.IssuerFromRequest, interceptors...))
router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(healthEndpoint, healthHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage()))
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o))
router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").Handler(intercept(authorizeCallbackHandler(o))) router.NewRoute().Path(authCallbackPath(o)).Queries("id", "{id}").HandlerFunc(authorizeCallbackHandler(o))
router.Handle(o.TokenEndpoint().Relative(), intercept(tokenHandler(o))) router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o))
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o)) router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o)) router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(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.KeysEndpoint().Relative(), keysHandler(o.Storage()))
return router return router
} }
// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login // AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login
func AuthCallbackURL(o OpenIDProvider) func(string) string { func AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string {
return func(requestID string) string { return func(ctx context.Context, requestID string) string {
return o.AuthorizationEndpoint().Absolute(o.Issuer()) + authCallbackPathSuffix + "?id=" + requestID return o.AuthorizationEndpoint().Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID
} }
} }
@ -109,7 +110,6 @@ func authCallbackPath(o OpenIDProvider) string {
} }
type Config struct { type Config struct {
Issuer string
CryptoKey [32]byte CryptoKey [32]byte
DefaultLogoutRedirectURI string DefaultLogoutRedirectURI string
CodeMethodS256 bool CodeMethodS256 bool
@ -133,6 +133,7 @@ type endpoints struct {
// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler()) // NewOpenIDProvider creates a provider. The provider provides (with HttpHandler())
// a http.Router that handles a suite of endpoints (some paths can be overridden): // a http.Router that handles a suite of endpoints (some paths can be overridden):
//
// /healthz // /healthz
// /ready // /ready
// /.well-known/openid-configuration // /.well-known/openid-configuration
@ -144,18 +145,22 @@ type endpoints struct {
// /revoke // /revoke
// /end_session // /end_session
// /keys // /keys
//
// This does not include login. Login is handled with a redirect that includes the // 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(). // 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 // Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back // op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter. // 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) { func NewOpenIDProvider(ctx context.Context, issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
err := ValidateIssuer(config.Issuer) return newProvider(ctx, config, storage, StaticIssuer(issuer), opOpts...)
if err != nil {
return nil, err
} }
o := &openidProvider{ func NewDynamicOpenIDProvider(ctx context.Context, path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return newProvider(ctx, config, storage, IssuerFromHost(path), opOpts...)
}
func newProvider(ctx context.Context, config *Config, storage Storage, issuer func(bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) {
o := &Provider{
config: config, config: config,
storage: storage, storage: storage,
endpoints: DefaultEndpoints, endpoints: DefaultEndpoints,
@ -168,9 +173,10 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO
} }
} }
keyCh := make(chan jose.SigningKey) o.issuer, err = issuer(o.insecure)
go storage.GetSigningKey(ctx, keyCh) if err != nil {
o.signer = NewSigner(ctx, storage, keyCh) return nil, err
}
o.httpHandler = CreateRouter(o, o.interceptors...) o.httpHandler = CreateRouter(o, o.interceptors...)
@ -182,22 +188,17 @@ func NewOpenIDProvider(ctx context.Context, config *Config, storage Storage, opO
o.crypto = NewAESCrypto(config.CryptoKey) o.crypto = NewAESCrypto(config.CryptoKey)
// Avoid potential race conditions by calling these early // 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 return o, nil
} }
type openidProvider struct { type Provider struct {
config *Config config *Config
issuer IssuerFromRequest
insecure bool
endpoints *endpoints endpoints *endpoints
storage Storage storage Storage
signer Signer
idTokenHintVerifier IDTokenHintVerifier
jwtProfileVerifier JWTProfileVerifier
accessTokenVerifier AccessTokenVerifier
keySet *openIDKeySet keySet *openIDKeySet
crypto Crypto crypto Crypto
httpHandler http.Handler httpHandler http.Handler
@ -209,159 +210,149 @@ type openidProvider struct {
idTokenHintVerifierOpts []IDTokenHintVerifierOpt idTokenHintVerifierOpts []IDTokenHintVerifierOpt
} }
func (o *openidProvider) Issuer() string { func (o *Provider) IssuerFromRequest(r *http.Request) string {
return o.config.Issuer 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 return o.endpoints.Authorization
} }
func (o *openidProvider) TokenEndpoint() Endpoint { func (o *Provider) TokenEndpoint() Endpoint {
return o.endpoints.Token return o.endpoints.Token
} }
func (o *openidProvider) IntrospectionEndpoint() Endpoint { func (o *Provider) IntrospectionEndpoint() Endpoint {
return o.endpoints.Introspection return o.endpoints.Introspection
} }
func (o *openidProvider) UserinfoEndpoint() Endpoint { func (o *Provider) UserinfoEndpoint() Endpoint {
return o.endpoints.Userinfo return o.endpoints.Userinfo
} }
func (o *openidProvider) RevocationEndpoint() Endpoint { func (o *Provider) RevocationEndpoint() Endpoint {
return o.endpoints.Revocation return o.endpoints.Revocation
} }
func (o *openidProvider) EndSessionEndpoint() Endpoint { func (o *Provider) EndSessionEndpoint() Endpoint {
return o.endpoints.EndSession return o.endpoints.EndSession
} }
func (o *openidProvider) KeysEndpoint() Endpoint { func (o *Provider) KeysEndpoint() Endpoint {
return o.endpoints.JwksURI return o.endpoints.JwksURI
} }
func (o *openidProvider) AuthMethodPostSupported() bool { func (o *Provider) AuthMethodPostSupported() bool {
return o.config.AuthMethodPost return o.config.AuthMethodPost
} }
func (o *openidProvider) CodeMethodS256Supported() bool { func (o *Provider) CodeMethodS256Supported() bool {
return o.config.CodeMethodS256 return o.config.CodeMethodS256
} }
func (o *openidProvider) AuthMethodPrivateKeyJWTSupported() bool { func (o *Provider) AuthMethodPrivateKeyJWTSupported() bool {
return o.config.AuthMethodPrivateKeyJWT return o.config.AuthMethodPrivateKeyJWT
} }
func (o *openidProvider) TokenEndpointSigningAlgorithmsSupported() []string { func (o *Provider) TokenEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"} return []string{"RS256"}
} }
func (o *openidProvider) GrantTypeRefreshTokenSupported() bool { func (o *Provider) GrantTypeRefreshTokenSupported() bool {
return o.config.GrantTypeRefreshToken return o.config.GrantTypeRefreshToken
} }
func (o *openidProvider) GrantTypeTokenExchangeSupported() bool { func (o *Provider) GrantTypeTokenExchangeSupported() bool {
return false return false
} }
func (o *openidProvider) GrantTypeJWTAuthorizationSupported() bool { func (o *Provider) GrantTypeJWTAuthorizationSupported() bool {
return true return true
} }
func (o *openidProvider) GrantTypeClientCredentialsSupported() bool { func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *Provider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) GrantTypeClientCredentialsSupported() bool {
_, ok := o.storage.(ClientCredentialsStorage) _, ok := o.storage.(ClientCredentialsStorage)
return ok return ok
} }
func (o *openidProvider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool { func (o *Provider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
return true return true
} }
func (o *openidProvider) IntrospectionEndpointSigningAlgorithmsSupported() []string { func (o *Provider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"} return []string{"RS256"}
} }
func (o *openidProvider) RevocationAuthMethodPrivateKeyJWTSupported() bool { func (o *Provider) RequestObjectSupported() bool {
return true
}
func (o *openidProvider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *openidProvider) RequestObjectSupported() bool {
return o.config.RequestObjectSupported return o.config.RequestObjectSupported
} }
func (o *openidProvider) RequestObjectSigningAlgorithmsSupported() []string { func (o *Provider) RequestObjectSigningAlgorithmsSupported() []string {
return []string{"RS256"} return []string{"RS256"}
} }
func (o *openidProvider) SupportedUILocales() []language.Tag { func (o *Provider) SupportedUILocales() []language.Tag {
return o.config.SupportedUILocales return o.config.SupportedUILocales
} }
func (o *openidProvider) Storage() Storage { func (o *Provider) Storage() Storage {
return o.storage return o.storage
} }
func (o *openidProvider) Decoder() httphelper.Decoder { func (o *Provider) Decoder() httphelper.Decoder {
return o.decoder return o.decoder
} }
func (o *openidProvider) Encoder() httphelper.Encoder { func (o *Provider) Encoder() httphelper.Encoder {
return o.encoder return o.encoder
} }
func (o *openidProvider) IDTokenHintVerifier() IDTokenHintVerifier { func (o *Provider) IDTokenHintVerifier(ctx context.Context) IDTokenHintVerifier {
if o.idTokenHintVerifier == nil { return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
o.idTokenHintVerifier = NewIDTokenHintVerifier(o.Issuer(), o.openIDKeySet(), o.idTokenHintVerifierOpts...)
}
return o.idTokenHintVerifier
} }
func (o *openidProvider) JWTProfileVerifier() JWTProfileVerifier { func (o *Provider) JWTProfileVerifier(ctx context.Context) JWTProfileVerifier {
if o.jwtProfileVerifier == nil { return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second)
o.jwtProfileVerifier = NewJWTProfileVerifier(o.Storage(), o.Issuer(), 1*time.Hour, time.Second)
}
return o.jwtProfileVerifier
} }
func (o *openidProvider) AccessTokenVerifier() AccessTokenVerifier { func (o *Provider) AccessTokenVerifier(ctx context.Context) AccessTokenVerifier {
if o.accessTokenVerifier == nil { return NewAccessTokenVerifier(IssuerFromContext(ctx), o.openIDKeySet(), o.accessTokenVerifierOpts...)
o.accessTokenVerifier = NewAccessTokenVerifier(o.Issuer(), o.openIDKeySet(), o.accessTokenVerifierOpts...)
}
return o.accessTokenVerifier
} }
func (o *openidProvider) openIDKeySet() oidc.KeySet { func (o *Provider) openIDKeySet() oidc.KeySet {
if o.keySet == nil { if o.keySet == nil {
o.keySet = &openIDKeySet{o.Storage()} o.keySet = &openIDKeySet{o.Storage()}
} }
return o.keySet return o.keySet
} }
func (o *openidProvider) Crypto() Crypto { func (o *Provider) Crypto() Crypto {
return o.crypto return o.crypto
} }
func (o *openidProvider) DefaultLogoutRedirectURI() string { func (o *Provider) DefaultLogoutRedirectURI() string {
return o.config.DefaultLogoutRedirectURI return o.config.DefaultLogoutRedirectURI
} }
func (o *openidProvider) Signer() Signer { func (o *Provider) Probes() []ProbesFn {
return o.signer
}
func (o *openidProvider) Probes() []ProbesFn {
return []ProbesFn{ return []ProbesFn{
ReadySigner(o.Signer()),
ReadyStorage(o.Storage()), ReadyStorage(o.Storage()),
} }
} }
func (o *openidProvider) HttpHandler() http.Handler { func (o *Provider) HttpHandler() http.Handler {
return o.httpHandler return o.httpHandler
} }
@ -372,22 +363,31 @@ type openIDKeySet struct {
// VerifySignature implements the oidc.KeySet interface // VerifySignature implements the oidc.KeySet interface
// providing an implementation for the keys stored in the OP Storage 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) { 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 { if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err) return nil, fmt.Errorf("error fetching keys: %w", err)
} }
keyID, alg := oidc.GetKeyIDAndAlg(jws) 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 { if err != nil {
return nil, fmt.Errorf("invalid signature: %w", err) return nil, fmt.Errorf("invalid signature: %w", err)
} }
return jws.Verify(&key) 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 { func WithCustomAuthEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -397,7 +397,7 @@ func WithCustomAuthEndpoint(endpoint Endpoint) Option {
} }
func WithCustomTokenEndpoint(endpoint Endpoint) Option { func WithCustomTokenEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -407,7 +407,7 @@ func WithCustomTokenEndpoint(endpoint Endpoint) Option {
} }
func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option { func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -417,7 +417,7 @@ func WithCustomIntrospectionEndpoint(endpoint Endpoint) Option {
} }
func WithCustomUserinfoEndpoint(endpoint Endpoint) Option { func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -427,7 +427,7 @@ func WithCustomUserinfoEndpoint(endpoint Endpoint) Option {
} }
func WithCustomRevocationEndpoint(endpoint Endpoint) Option { func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -437,7 +437,7 @@ func WithCustomRevocationEndpoint(endpoint Endpoint) Option {
} }
func WithCustomEndSessionEndpoint(endpoint Endpoint) Option { func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -447,7 +447,7 @@ func WithCustomEndSessionEndpoint(endpoint Endpoint) Option {
} }
func WithCustomKeysEndpoint(endpoint Endpoint) Option { func WithCustomKeysEndpoint(endpoint Endpoint) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
if err := endpoint.Validate(); err != nil { if err := endpoint.Validate(); err != nil {
return err return err
} }
@ -457,7 +457,7 @@ func WithCustomKeysEndpoint(endpoint Endpoint) Option {
} }
func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys 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.Authorization = auth
o.endpoints.Token = token o.endpoints.Token = token
o.endpoints.Userinfo = userInfo o.endpoints.Userinfo = userInfo
@ -469,38 +469,32 @@ func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys End
} }
func WithHttpInterceptors(interceptors ...HttpInterceptor) Option { func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
o.interceptors = append(o.interceptors, interceptors...) o.interceptors = append(o.interceptors, interceptors...)
return nil return nil
} }
} }
func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option { func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
o.accessTokenVerifierOpts = opts o.accessTokenVerifierOpts = opts
return nil return nil
} }
} }
func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option { func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
return func(o *openidProvider) error { return func(o *Provider) error {
o.idTokenHintVerifierOpts = opts o.idTokenHintVerifierOpts = opts
return nil return nil
} }
} }
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler { func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler {
return func(handlerFunc http.HandlerFunc) http.Handler { issuerInterceptor := NewIssuerInterceptor(i)
handler := handlerFuncToHandler(handlerFunc) return func(handler http.Handler) http.Handler {
for i := len(interceptors) - 1; i >= 0; i-- { for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler) 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)
})
}

View file

@ -5,7 +5,7 @@ import (
"errors" "errors"
"net/http" "net/http"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
) )
type ProbesFn func(context.Context) error type ProbesFn func(context.Context) error
@ -31,15 +31,6 @@ func Readiness(w http.ResponseWriter, r *http.Request, probes ...ProbesFn) {
ok(w) 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 { func ReadyStorage(s Storage) ProbesFn {
return func(ctx context.Context) error { return func(ctx context.Context) error {
if s == nil { if s == nil {

View file

@ -5,14 +5,14 @@ import (
"net/http" "net/http"
"net/url" "net/url"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type SessionEnder interface { type SessionEnder interface {
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Storage() Storage Storage() Storage
IDTokenHintVerifier() IDTokenHintVerifier IDTokenHintVerifier(context.Context) IDTokenHintVerifier
DefaultLogoutRedirectURI() string DefaultLogoutRedirectURI() string
} }
@ -59,7 +59,7 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest,
RedirectURI: ender.DefaultLogoutRedirectURI(), RedirectURI: ender.DefaultLogoutRedirectURI(),
} }
if req.IdTokenHint != "" { if req.IdTokenHint != "" {
claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier()) claims, err := VerifyIDTokenHint(ctx, req.IdTokenHint, ender.IDTokenHintVerifier(ctx))
if err != nil { if err != nil {
return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err) return nil, oidc.ErrInvalidRequest().WithDescription("id_token_hint invalid").WithParent(err)
} }

View file

@ -1,88 +1,38 @@
package op package op
import ( import (
"context"
"errors" "errors"
"sync"
"github.com/zitadel/logging"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
) )
type Signer interface { var (
Health(ctx context.Context) error ErrSignerCreationFailed = errors.New("signer creation failed")
Signer() jose.Signer )
type SigningKey interface {
SignatureAlgorithm() jose.SignatureAlgorithm SignatureAlgorithm() jose.SignatureAlgorithm
Key() interface{}
ID() string
} }
type tokenSigner struct { func SignerFromKey(key SigningKey) (jose.Signer, error) {
signer jose.Signer signer, err := jose.NewSigner(jose.SigningKey{
storage AuthStorage Algorithm: key.SignatureAlgorithm(),
alg jose.SignatureAlgorithm Key: &jose.JSONWebKey{
lock sync.RWMutex Key: key.Key(),
} KeyID: key.ID(),
},
func NewSigner(ctx context.Context, storage AuthStorage, keyCh <-chan jose.SigningKey) Signer { }, &jose.SignerOptions{})
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{})
if err != nil { if err != nil {
logging.New().WithError(err).Error("error creating signer") return nil, ErrSignerCreationFailed //TODO: log / wrap error?
return
} }
logging.Info("signer exchanged signing key") return signer, nil
} }
func (s *tokenSigner) SignatureAlgorithm() jose.SignatureAlgorithm { type Key interface {
return s.alg ID() string
Algorithm() jose.SignatureAlgorithm
Use() string
Key() interface{}
} }

View file

@ -7,7 +7,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type AuthStorage interface { type AuthStorage interface {
@ -47,8 +47,14 @@ type AuthStorage interface {
// tokenOrTokenID will be the refresh token, not its ID. // tokenOrTokenID will be the refresh token, not its ID.
RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error RevokeToken(ctx context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error
GetSigningKey(context.Context, chan<- jose.SigningKey) SigningKey(context.Context) (SigningKey, error)
GetKeySet(context.Context) (*jose.JSONWebKeySet, 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)
} }
// CanRefreshTokenInfo is an optional additional interface that Storage can support. // CanRefreshTokenInfo is an optional additional interface that Storage can support.
@ -62,10 +68,6 @@ type CanRefreshTokenInfo interface {
var ErrInvalidRefreshToken = errors.New("invalid_refresh_token") var ErrInvalidRefreshToken = errors.New("invalid_refresh_token")
type ClientCredentialsStorage interface {
ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (TokenRequest, error)
}
type OPStorage interface { type OPStorage interface {
GetClientByClientID(ctx context.Context, clientID string) (Client, error) GetClientByClientID(ctx context.Context, clientID string) (Client, error)
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
@ -80,6 +82,12 @@ type OPStorage interface {
ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, 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 // Storage is a required parameter for NewOpenIDProvider(). In addition to the
// embedded interfaces below, if the passed Storage implements ClientCredentialsStorage // embedded interfaces below, if the passed Storage implements ClientCredentialsStorage
// then the grant type "client_credentials" will be supported. In that case, the access // then the grant type "client_credentials" will be supported. In that case, the access

View file

@ -4,14 +4,12 @@ import (
"context" "context"
"time" "time"
"github.com/zitadel/oidc/pkg/crypto" "github.com/zitadel/oidc/v2/pkg/crypto"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/strings" "github.com/zitadel/oidc/v2/pkg/strings"
) )
type TokenCreator interface { type TokenCreator interface {
Issuer() string
Signer() Signer
Storage() Storage Storage() Storage
Crypto() Crypto Crypto() Crypto
} }
@ -22,6 +20,13 @@ type TokenRequest interface {
GetScopes() []string 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) { func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Client, creator TokenCreator, createAccessToken bool, code, refreshToken string) (*oidc.AccessTokenResponse, error) {
var accessToken, newRefreshToken string var accessToken, newRefreshToken string
var validity time.Duration var validity time.Duration
@ -32,7 +37,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -57,7 +62,7 @@ func CreateTokenResponse(ctx context.Context, request IDTokenRequest, client Cli
}, nil }, 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) { if needsRefreshToken(tokenRequest, client) {
return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken) return storage.CreateAccessAndRefreshTokens(ctx, tokenRequest, refreshToken)
} }
@ -65,7 +70,7 @@ func createTokens(ctx context.Context, tokenRequest TokenRequest, storage Storag
return return
} }
func needsRefreshToken(tokenRequest TokenRequest, client Client) bool { func needsRefreshToken(tokenRequest TokenRequest, client AccessTokenClient) bool {
switch req := tokenRequest.(type) { switch req := tokenRequest.(type) {
case AuthRequest: case AuthRequest:
return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken) return strings.Contains(req.GetScopes(), oidc.ScopeOfflineAccess) && req.GetResponseType() == oidc.ResponseTypeCode && ValidateGrantType(client, oidc.GrantTypeRefreshToken)
@ -76,7 +81,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) id, newRefreshToken, exp, err := createTokens(ctx, tokenRequest, creator.Storage(), refreshToken, client)
if err != nil { if err != nil {
return "", "", 0, err return "", "", 0, err
@ -87,7 +92,7 @@ func CreateAccessToken(ctx context.Context, tokenRequest TokenRequest, accessTok
} }
validity = exp.Add(clockSkew).Sub(time.Now().UTC()) validity = exp.Add(clockSkew).Sub(time.Now().UTC())
if accessTokenType == AccessTokenTypeJWT { 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 return
} }
accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) accessToken, err = CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto())
@ -98,7 +103,7 @@ func CreateBearerToken(tokenID, subject string, crypto Crypto) (string, error) {
return crypto.Encrypt(tokenID + ":" + subject) 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()) claims := oidc.NewAccessTokenClaims(issuer, tokenRequest.GetSubject(), tokenRequest.GetAudience(), exp, id, client.GetID(), client.ClockSkew())
if client != nil { if client != nil {
restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes()) restrictedScopes := client.RestrictAdditionalAccessTokenScopes()(tokenRequest.GetScopes())
@ -108,7 +113,15 @@ func CreateJWT(ctx context.Context, issuer string, tokenRequest TokenRequest, ex
} }
claims.SetPrivateClaims(privateClaims) claims.SetPrivateClaims(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 { type IDTokenRequest interface {
@ -120,7 +133,7 @@ type IDTokenRequest interface {
GetSubject() string 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) exp := time.Now().UTC().Add(client.ClockSkew()).Add(validity)
var acr, nonce string var acr, nonce string
if authRequest, ok := request.(AuthRequest); ok { if authRequest, ok := request.(AuthRequest); ok {
@ -129,8 +142,12 @@ 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()) claims := oidc.NewIDTokenClaims(issuer, request.GetSubject(), request.GetAudience(), exp, request.GetAuthTime(), nonce, acr, request.GetAMR(), request.GetClientID(), client.ClockSkew())
scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes()) scopes := client.RestrictAdditionalIdTokenScopes()(request.GetScopes())
signingKey, err := storage.SigningKey(ctx)
if err != nil {
return "", err
}
if accessToken != "" { if accessToken != "" {
atHash, err := oidc.ClaimHash(accessToken, signer.SignatureAlgorithm()) atHash, err := oidc.ClaimHash(accessToken, signingKey.SignatureAlgorithm())
if err != nil { if err != nil {
return "", err return "", err
} }
@ -148,14 +165,17 @@ func CreateIDToken(ctx context.Context, issuer string, request IDTokenRequest, v
claims.SetUserinfo(userInfo) claims.SetUserinfo(userInfo)
} }
if code != "" { if code != "" {
codeHash, err := oidc.ClaimHash(code, signer.SignatureAlgorithm()) codeHash, err := oidc.ClaimHash(code, signingKey.SignatureAlgorithm())
if err != nil { if err != nil {
return "", err return "", err
} }
claims.SetCodeHash(codeHash) claims.SetCodeHash(codeHash)
} }
signer, err := SignerFromKey(signingKey)
return crypto.Sign(claims, signer.Signer()) if err != nil {
return "", err
}
return crypto.Sign(claims, signer)
} }
func removeUserinfoScopes(scopes []string) []string { func removeUserinfoScopes(scopes []string) []string {

View file

@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// ClientCredentialsExchange handles the OAuth 2.0 client_credentials grant, including // 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 return request, nil
} }
// ValidateClientCredentialsRequest validates the refresh_token request parameters including authorization check of the client // ValidateClientCredentialsRequest validates the client_credentials request parameters including authorization check of the client
// and returns the data representing the original auth request corresponding to the refresh_token // 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) { func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (TokenRequest, Client, error) {
storage, ok := exchanger.Storage().(ClientCredentialsStorage) storage, ok := exchanger.Storage().(ClientCredentialsStorage)
if !ok { if !ok {
return nil, nil, oidc.ErrUnsupportedGrantType().WithDescription("client_credentials grant not supported") 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -84,12 +84,8 @@ func ValidateClientCredentialsRequest(ctx context.Context, request *oidc.ClientC
return tokenRequest, client, nil return tokenRequest, client, nil
} }
func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, exchanger Exchanger) (Client, error) { func AuthorizeClientCredentialsClient(ctx context.Context, request *oidc.ClientCredentialsRequest, storage ClientCredentialsStorage) (Client, error) {
if err := AuthorizeClientIDSecret(ctx, request.ClientID, request.ClientSecret, exchanger.Storage()); err != nil { client, err := storage.ClientCredentials(ctx, request.ClientID, request.ClientSecret)
return nil, err
}
client, err := exchanger.Storage().GetClientByClientID(ctx, request.ClientID)
if err != nil { if err != nil {
return nil, oidc.ErrInvalidClient().WithParent(err) 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -4,8 +4,8 @@ import (
"context" "context"
"net/http" "net/http"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
// CodeExchange handles the OAuth 2.0 authorization_code grant, including // CodeExchange handles the OAuth 2.0 authorization_code grant, including

View file

@ -1,24 +1,25 @@
package op package op
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"net/url" "net/url"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type Introspector interface { type Introspector interface {
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Crypto() Crypto Crypto() Crypto
Storage() Storage Storage() Storage
AccessTokenVerifier() AccessTokenVerifier AccessTokenVerifier(context.Context) AccessTokenVerifier
} }
type IntrospectorJWTProfile interface { type IntrospectorJWTProfile interface {
Introspector Introspector
JWTProfileVerifier() JWTProfileVerifier JWTProfileVerifier(context.Context) JWTProfileVerifier
} }
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
@ -62,7 +63,7 @@ func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector)
return "", "", errors.New("unable to parse request") return "", "", errors.New("unable to parse request")
} }
if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" { if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier()) profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier(r.Context()))
if err == nil { if err == nil {
return req.Token, profile.Issuer, nil return req.Token, profile.Issuer, nil
} }

View file

@ -5,13 +5,13 @@ import (
"net/http" "net/http"
"time" "time"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type JWTAuthorizationGrantExchanger interface { type JWTAuthorizationGrantExchanger interface {
Exchanger 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 // 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) 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 { if err != nil {
RequestError(w, r, err) RequestError(w, r, err)
return return
@ -53,21 +53,35 @@ func ParseJWTProfileGrantRequest(r *http.Request, decoder httphelper.Decoder) (*
return tokenReq, nil 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) { func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator) (*oidc.AccessTokenResponse, error) {
id, exp, err := creator.Storage().CreateAccessToken(ctx, tokenRequest) // return an opaque token as default to not break current implementations
if err != nil { tokenType := AccessTokenTypeBearer
return nil, err
} // the current CreateAccessToken function, esp. CreateJWT requires an implementation of an AccessTokenClient
accessToken, err := CreateBearerToken(id, tokenRequest.GetSubject(), creator.Crypto()) client := &jwtProfileClient{
if err != nil { id: tokenRequest.GetSubject(),
return nil, err
} }
// 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{ return &oidc.AccessTokenResponse{
AccessToken: accessToken, AccessToken: accessToken,
TokenType: oidc.BearerToken, TokenType: oidc.BearerToken,
ExpiresIn: uint64(exp.Sub(time.Now().UTC()).Seconds()), ExpiresIn: uint64(validity.Seconds()),
}, nil }, nil
} }
@ -77,3 +91,27 @@ func CreateJWTTokenResponse(ctx context.Context, tokenRequest TokenRequest, crea
func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) { func ParseJWTProfileRequest(r *http.Request, decoder httphelper.Decoder) (*oidc.JWTProfileGrantRequest, error) {
return ParseJWTProfileGrantRequest(r, decoder) 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,
}
}

View file

@ -6,9 +6,9 @@ import (
"net/http" "net/http"
"time" "time"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/pkg/strings" "github.com/zitadel/oidc/v2/pkg/strings"
) )
type RefreshTokenRequest interface { type RefreshTokenRequest interface {

View file

@ -5,15 +5,13 @@ import (
"net/http" "net/http"
"net/url" "net/url"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type Exchanger interface { type Exchanger interface {
Issuer() string
Storage() Storage Storage() Storage
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Signer() Signer
Crypto() Crypto Crypto() Crypto
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
AuthMethodPrivateKeyJWTSupported() bool AuthMethodPrivateKeyJWTSupported() bool
@ -122,7 +120,7 @@ func AuthorizeCodeChallenge(tokenReq *oidc.AccessTokenRequest, challenge *oidc.C
// AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously // AuthorizePrivateJWTKey authorizes a client by validating the client_assertion's signature with a previously
// registered public key (JWT Profile) // registered public key (JWT Profile)
func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchanger JWTAuthorizationGrantExchanger) (Client, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -136,8 +134,8 @@ func AuthorizePrivateJWTKey(ctx context.Context, clientAssertion string, exchang
return client, nil return client, nil
} }
// ValidateGrantType ensures that the requested grant_type is allowed by the Client // ValidateGrantType ensures that the requested grant_type is allowed by the client
func ValidateGrantType(client Client, grantType oidc.GrantType) bool { func ValidateGrantType(client interface{ GrantTypes() []oidc.GrantType }, grantType oidc.GrantType) bool {
if client == nil { if client == nil {
return false return false
} }

View file

@ -7,22 +7,22 @@ import (
"net/url" "net/url"
"strings" "strings"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type Revoker interface { type Revoker interface {
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Crypto() Crypto Crypto() Crypto
Storage() Storage Storage() Storage
AccessTokenVerifier() AccessTokenVerifier AccessTokenVerifier(context.Context) AccessTokenVerifier
AuthMethodPrivateKeyJWTSupported() bool AuthMethodPrivateKeyJWTSupported() bool
AuthMethodPostSupported() bool AuthMethodPostSupported() bool
} }
type RevokerJWTProfile interface { type RevokerJWTProfile interface {
Revoker Revoker
JWTProfileVerifier() JWTProfileVerifier JWTProfileVerifier(context.Context) JWTProfileVerifier
} }
func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) { func revocationHandler(revoker Revoker) func(http.ResponseWriter, *http.Request) {
@ -87,7 +87,7 @@ func ParseTokenRevocationRequest(r *http.Request, revoker Revoker) (token, token
if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() { if !ok || !revoker.AuthMethodPrivateKeyJWTSupported() {
return "", "", "", oidc.ErrInvalidClient().WithDescription("auth_method private_key_jwt not supported") 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 { if err == nil {
return req.Token, req.TokenTypeHint, profile.Issuer, nil return req.Token, req.TokenTypeHint, profile.Issuer, nil
} }
@ -151,7 +151,7 @@ func getTokenIDAndSubjectForRevocation(ctx context.Context, userinfoProvider Use
} }
return splitToken[0], splitToken[1], true return splitToken[0], splitToken[1], true
} }
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier()) accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
if err != nil { if err != nil {
return "", "", false return "", "", false
} }

View file

@ -6,15 +6,15 @@ import (
"net/http" "net/http"
"strings" "strings"
httphelper "github.com/zitadel/oidc/pkg/http" httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type UserinfoProvider interface { type UserinfoProvider interface {
Decoder() httphelper.Decoder Decoder() httphelper.Decoder
Crypto() Crypto Crypto() Crypto
Storage() Storage Storage() Storage
AccessTokenVerifier() AccessTokenVerifier AccessTokenVerifier(context.Context) AccessTokenVerifier
} }
func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) { func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter, *http.Request) {
@ -81,7 +81,7 @@ func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider
} }
return splitToken[0], splitToken[1], true return splitToken[0], splitToken[1], true
} }
accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier()) accessTokenClaims, err := VerifyAccessToken(ctx, accessToken, userinfoProvider.AccessTokenVerifier(ctx))
if err != nil { if err != nil {
return "", "", false return "", "", false
} }

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type AccessTokenVerifier interface { type AccessTokenVerifier interface {

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type IDTokenHintVerifier interface { type IDTokenHintVerifier interface {

View file

@ -8,7 +8,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/oidc"
) )
type JWTProfileVerifier interface { type JWTProfileVerifier interface {