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:
parent
a34d7a1630
commit
1165d88c69
77 changed files with 2349 additions and 913 deletions
113
example/server/dynamic/login.go
Normal file
113
example/server/dynamic/login.go
Normal 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)
|
||||
}
|
138
example/server/dynamic/op.go
Normal file
138
example/server/dynamic/op.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")),
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
260
example/server/storage/storage_dynamic.go
Normal file
260
example/server/storage/storage_dynamic.go
Normal 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
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue