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

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

View file

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

View file

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

View file

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

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)
/github example of the extended OAuth2 library, providing an HTTP client with a reuse token source
/service demonstration of JWT Profile Authorization Grant
/server example of an OpenID Provider implementation including some very basic login UI
/server examples of an OpenID Provider implementations (including dynamic) with some very basic
*/
package example

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

View file

@ -5,13 +5,12 @@ import (
"crypto/sha256"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
"golang.org/x/text/language"
"github.com/zitadel/oidc/example/server/storage"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/example/server/storage"
"github.com/zitadel/oidc/v2/pkg/op"
)
const (
@ -35,9 +34,6 @@ type Storage interface {
//
// Use one of the pre-made clients in storage/clients.go or register a new one.
func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Router {
// this will allow us to use an issuer with http:// instead of https://
os.Setenv(op.OidcDevMode, "true")
// 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"))
@ -81,7 +77,6 @@ func SetupServer(ctx context.Context, issuer string, storage Storage) *mux.Route
// it will enable all options (see descriptions)
func newOP(ctx context.Context, storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) {
config := &op.Config{
Issuer: issuer,
CryptoKey: key,
// will be used if the end_session endpoint is called without a post_logout_redirect_uri
@ -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
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
op.WithCustomAuthEndpoint(op.NewEndpoint("auth")),
)

View file

@ -2,23 +2,28 @@ package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/zitadel/oidc/example/server/exampleop"
"github.com/zitadel/oidc/example/server/storage"
"github.com/zitadel/oidc/v2/example/server/exampleop"
"github.com/zitadel/oidc/v2/example/server/storage"
)
func main() {
ctx := context.Background()
//we will run on :9998
port := "9998"
//which gives us the issuer: //http://localhost:9998/
issuer := fmt.Sprintf("http://localhost:%s/", port)
// the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations
// this might be the layer for accessing your database
// in this example it will be handled in-memory
storage := storage.NewStorage(storage.NewUserStore())
storage := storage.NewStorage(storage.NewUserStore(issuer))
port := "9998"
router := exampleop.SetupServer(ctx, "http://localhost:"+port, storage)
router := exampleop.SetupServer(ctx, issuer, storage)
server := &http.Server{
Addr: ":" + port,

View file

@ -3,8 +3,8 @@ package storage
import (
"time"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
var (
@ -113,14 +113,14 @@ func (c *Client) IsScopeAllowed(scope string) bool {
// IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token
// even if an access token if issued which violates the OIDC Core spec
//(5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
// (5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims)
// some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued
func (c *Client) IDTokenUserinfoClaimsAssertion() bool {
return c.idTokenUserinfoClaimsAssertion
}
// ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations
//(subtract from issued_at, add to expiration, ...)
// (subtract from issued_at, add to expiration, ...)
func (c *Client) ClockSkew() time.Duration {
return c.clockSkew
}
@ -141,7 +141,7 @@ func RegisterClients(registerClients ...*Client) {
// user-defined redirectURIs may include:
// - http://localhost without port specification (e.g. http://localhost/auth/callback)
// - custom protocol (e.g. custom://auth/callback)
//(the examples will be used as default, if none is provided)
// (the examples will be used as default, if none is provided)
func NativeClient(id string, redirectURIs ...string) *Client {
if len(redirectURIs) == 0 {
redirectURIs = []string{
@ -168,7 +168,7 @@ func NativeClient(id string, redirectURIs ...string) *Client {
// WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens
// user-defined redirectURIs may include:
// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback)
//(the example will be used as default, if none is provided)
// (the example will be used as default, if none is provided)
func WebClient(id, secret string, redirectURIs ...string) *Client {
if len(redirectURIs) == 0 {
redirectURIs = []string{

View file

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

View file

@ -12,8 +12,8 @@ import (
"github.com/google/uuid"
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/oidc/pkg/oidc"
"github.com/zitadel/oidc/pkg/op"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
)
// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant
@ -45,9 +45,41 @@ type Storage struct {
}
type signingKey struct {
ID string
Algorithm string
Key *rsa.PrivateKey
id string
algorithm jose.SignatureAlgorithm
key *rsa.PrivateKey
}
func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm {
return s.algorithm
}
func (s *signingKey) Key() interface{} {
return s.key
}
func (s *signingKey) ID() string {
return s.id
}
type publicKey struct {
signingKey
}
func (s *publicKey) ID() string {
return s.id
}
func (s *publicKey) Algorithm() jose.SignatureAlgorithm {
return s.algorithm
}
func (s *publicKey) Use() string {
return "sig"
}
func (s *publicKey) Key() interface{} {
return &s.key.PublicKey
}
func NewStorage(userStore UserStore) *Storage {
@ -67,9 +99,9 @@ func NewStorage(userStore UserStore) *Storage {
},
},
signingKey: signingKey{
ID: "id",
Algorithm: "RS256",
Key: key,
id: uuid.NewString(),
algorithm: jose.RS256,
key: key,
},
}
}
@ -288,41 +320,29 @@ func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID
return nil
}
// GetSigningKey implements the op.Storage interface
// SigningKey implements the op.Storage interface
// it will be called when creating the OpenID Provider
func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) {
func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) {
// in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256
// you would obviously have a more complex implementation and store / retrieve the key from your database as well
//
// the idea of the signing key channel is, that you can (with what ever mechanism) rotate your signing key and
// switch the key of the signer via this channel
keyCh <- jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(s.signingKey.Algorithm), // always tell the signer with algorithm to use
Key: jose.JSONWebKey{
KeyID: s.signingKey.ID, // always give the key an id so, that it will include it in the token header as `kid` claim
Key: s.signingKey.Key,
},
}
return &s.signingKey, nil
}
// GetKeySet implements the op.Storage interface
// SignatureAlgorithms implements the op.Storage interface
// it will be called to get the sign
func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) {
return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil
}
// KeySet implements the op.Storage interface
// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ...
func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) {
func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) {
// as mentioned above, this example only has a single signing key without key rotation,
// so it will directly use its public key
//
// when using key rotation you typically would store the public keys alongside the private keys in your database
// and give both of them an expiration date, with the public key having a longer lifetime (e.g. rotate private key every
return &jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{
{
KeyID: s.signingKey.ID,
Algorithm: s.signingKey.Algorithm,
Use: oidc.KeyUseSignature,
Key: &s.signingKey.Key.PublicKey,
},
},
}, nil
//and give both of them an expiration date, with the public key having a longer lifetime
return []op.Key{&publicKey{s.signingKey}}, nil
}
// GetClientByClientID implements the op.Storage interface

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