* 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>
610 lines
22 KiB
Go
610 lines
22 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
"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
|
|
// the corresponding private key is in the service-key1.json (for demonstration purposes)
|
|
var serviceKey1 = &rsa.PublicKey{
|
|
N: func() *big.Int {
|
|
n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16)
|
|
return n
|
|
}(),
|
|
E: 65537,
|
|
}
|
|
|
|
// var _ op.Storage = &storage{}
|
|
// var _ op.ClientCredentialsStorage = &storage{}
|
|
|
|
// storage implements the op.Storage interface
|
|
// typically you would implement this as a layer on top of your database
|
|
// for simplicity this example keeps everything in-memory
|
|
type Storage struct {
|
|
lock sync.Mutex
|
|
authRequests map[string]*AuthRequest
|
|
codes map[string]string
|
|
tokens map[string]*Token
|
|
clients map[string]*Client
|
|
userStore UserStore
|
|
services map[string]Service
|
|
refreshTokens map[string]*RefreshToken
|
|
signingKey signingKey
|
|
}
|
|
|
|
type signingKey struct {
|
|
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 {
|
|
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
return &Storage{
|
|
authRequests: make(map[string]*AuthRequest),
|
|
codes: make(map[string]string),
|
|
tokens: make(map[string]*Token),
|
|
refreshTokens: make(map[string]*RefreshToken),
|
|
clients: clients,
|
|
userStore: userStore,
|
|
services: map[string]Service{
|
|
userStore.ExampleClientID(): {
|
|
keys: map[string]*rsa.PublicKey{
|
|
"key1": serviceKey1,
|
|
},
|
|
},
|
|
},
|
|
signingKey: signingKey{
|
|
id: uuid.NewString(),
|
|
algorithm: jose.RS256,
|
|
key: key,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CheckUsernamePassword implements the `authenticate` interface of the login
|
|
func (s *Storage) CheckUsernamePassword(username, password, id string) error {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
request, ok := s.authRequests[id]
|
|
if !ok {
|
|
return fmt.Errorf("request not found")
|
|
}
|
|
|
|
// for demonstration purposes we'll check we'll have a simple user store and
|
|
// a plain text password. For real world scenarios, be sure to have the password
|
|
// hashed and salted (e.g. using bcrypt)
|
|
user := s.userStore.GetUserByUsername(username)
|
|
if user != nil && user.Password == password {
|
|
// be sure to set user id into the auth request after the user was checked,
|
|
// so that you'll be able to get more information about the user after the login
|
|
request.UserID = user.ID
|
|
|
|
// you will have to change some state on the request to guide the user through possible multiple steps of the login process
|
|
// in this example we'll simply check the username / password and set a boolean to true
|
|
// therefore we will also just check this boolean if the request / login has been finished
|
|
request.passwordChecked = true
|
|
return nil
|
|
}
|
|
return fmt.Errorf("username or password wrong")
|
|
}
|
|
|
|
// CreateAuthRequest implements the op.Storage interface
|
|
// it will be called after parsing and validation of the authentication request
|
|
func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
|
|
// typically, you'll fill your storage / storage model with the information of the passed object
|
|
request := authRequestToInternal(authReq, userID)
|
|
|
|
// you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid)
|
|
request.ID = uuid.NewString()
|
|
|
|
// and save it in your database (for demonstration purposed we will use a simple map)
|
|
s.authRequests[request.ID] = request
|
|
|
|
// finally, return the request (which implements the AuthRequest interface of the OP
|
|
return request, nil
|
|
}
|
|
|
|
// AuthRequestByID implements the op.Storage interface
|
|
// it will be called after the Login UI redirects back to the OIDC endpoint
|
|
func (s *Storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
request, ok := s.authRequests[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("request not found")
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
// 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 *Storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) {
|
|
// for this example we read the id by code and then get the request by id
|
|
requestID, ok := func() (string, bool) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
requestID, ok := s.codes[code]
|
|
return requestID, ok
|
|
}()
|
|
if !ok {
|
|
return nil, fmt.Errorf("code invalid or expired")
|
|
}
|
|
return s.AuthRequestByID(ctx, requestID)
|
|
}
|
|
|
|
// 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 *Storage) SaveAuthCode(ctx context.Context, id string, code string) error {
|
|
// for this example we'll just save the authRequestID to the code
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.codes[code] = id
|
|
return nil
|
|
}
|
|
|
|
// 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 *Storage) DeleteAuthRequest(ctx context.Context, id string) error {
|
|
// you can simply delete all reference to the auth request
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
delete(s.authRequests, id)
|
|
for code, requestID := range s.codes {
|
|
if id == requestID {
|
|
delete(s.codes, code)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
|
|
var applicationID string
|
|
// if authenticated for an app (auth code / implicit flow) we must save the client_id to the token
|
|
authReq, ok := request.(*AuthRequest)
|
|
if ok {
|
|
applicationID = authReq.ApplicationID
|
|
}
|
|
token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes())
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
return token.ID, token.Expiration, nil
|
|
}
|
|
|
|
// CreateAccessAndRefreshTokens implements the op.Storage interface
|
|
// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request)
|
|
func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
|
// get the information depending on the request type / implementation
|
|
applicationID, authTime, amr := getInfoFromRequest(request)
|
|
|
|
// if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token
|
|
if currentRefreshToken == "" {
|
|
refreshTokenID := uuid.NewString()
|
|
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
|
if err != nil {
|
|
return "", "", time.Time{}, err
|
|
}
|
|
refreshToken, err := s.createRefreshToken(accessToken, amr, authTime)
|
|
if err != nil {
|
|
return "", "", time.Time{}, err
|
|
}
|
|
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
|
}
|
|
|
|
// if we get here, the currentRefreshToken was not empty, so the call is a refresh token request
|
|
// we therefore will have to check the currentRefreshToken and renew the refresh token
|
|
refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken)
|
|
if err != nil {
|
|
return "", "", time.Time{}, err
|
|
}
|
|
accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes())
|
|
if err != nil {
|
|
return "", "", time.Time{}, err
|
|
}
|
|
return accessToken.ID, refreshToken, accessToken.Expiration, nil
|
|
}
|
|
|
|
// TokenRequestByRefreshToken implements the op.Storage interface
|
|
// it will be called after parsing and validation of the refresh token request
|
|
func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
token, ok := s.refreshTokens[refreshToken]
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid refresh_token")
|
|
}
|
|
return RefreshTokenRequestFromBusiness(token), nil
|
|
}
|
|
|
|
// 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 *Storage) TerminateSession(ctx context.Context, userID string, clientID string) error {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
for _, token := range s.tokens {
|
|
if token.ApplicationID == clientID && token.Subject == userID {
|
|
delete(s.tokens, token.ID)
|
|
delete(s.refreshTokens, token.RefreshTokenID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RevokeToken implements the op.Storage interface
|
|
// it will be called after parsing and validation of the token revocation request
|
|
func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error {
|
|
// a single token was requested to be removed
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
accessToken, ok := s.tokens[tokenIDOrToken] // tokenID
|
|
if ok {
|
|
if accessToken.ApplicationID != clientID {
|
|
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
|
}
|
|
// if it is an access token, just remove it
|
|
// you could also remove the corresponding refresh token if really necessary
|
|
delete(s.tokens, accessToken.ID)
|
|
return nil
|
|
}
|
|
refreshToken, ok := s.refreshTokens[tokenIDOrToken] // token
|
|
if !ok {
|
|
// if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of
|
|
// being not valid (anymore) is achieved
|
|
return nil
|
|
}
|
|
if refreshToken.ApplicationID != clientID {
|
|
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
|
}
|
|
// if it is a refresh token, you will have to remove the access token as well
|
|
delete(s.refreshTokens, refreshToken.ID)
|
|
for _, accessToken := range s.tokens {
|
|
if accessToken.RefreshTokenID == refreshToken.ID {
|
|
delete(s.tokens, accessToken.ID)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SigningKey implements the op.Storage interface
|
|
// it will be called when creating the OpenID Provider
|
|
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
|
|
return &s.signingKey, nil
|
|
}
|
|
|
|
// 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) 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
|
|
return []op.Key{&publicKey{s.signingKey}}, nil
|
|
}
|
|
|
|
// 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 *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
client, ok := s.clients[clientID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("client not found")
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// AuthorizeClientIDSecret implements the op.Storage interface
|
|
// it will be called for validating the client_id, client_secret on token or introspection requests
|
|
func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
client, ok := s.clients[clientID]
|
|
if !ok {
|
|
return fmt.Errorf("client not found")
|
|
}
|
|
// for this example we directly check the secret
|
|
// obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt)
|
|
if client.secret != clientSecret {
|
|
return fmt.Errorf("invalid secret")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetUserinfoFromScopes implements the op.Storage interface
|
|
// it will be called for the creation of an id_token, so we'll just pass it to the private function without any further check
|
|
func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error {
|
|
return s.setUserinfo(ctx, userinfo, userID, clientID, scopes)
|
|
}
|
|
|
|
// SetUserinfoFromToken implements the op.Storage interface
|
|
// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function
|
|
func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error {
|
|
token, ok := func() (*Token, bool) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
token, ok := s.tokens[tokenID]
|
|
return token, ok
|
|
}()
|
|
if !ok {
|
|
return fmt.Errorf("token is invalid or has expired")
|
|
}
|
|
// the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler,
|
|
// and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly
|
|
// note that the origin can be empty (if called by a web client)
|
|
//
|
|
// if origin != "" {
|
|
// client, ok := s.clients[token.ApplicationID]
|
|
// if !ok {
|
|
// return fmt.Errorf("client not found")
|
|
// }
|
|
// if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil {
|
|
// return err
|
|
// }
|
|
//}
|
|
return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes)
|
|
}
|
|
|
|
// SetIntrospectionFromToken implements the op.Storage interface
|
|
// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function
|
|
func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
|
token, ok := func() (*Token, bool) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
token, ok := s.tokens[tokenID]
|
|
return token, ok
|
|
}()
|
|
if !ok {
|
|
return fmt.Errorf("token is invalid or has expired")
|
|
}
|
|
// check if the client is part of the requested audience
|
|
for _, aud := range token.Audience {
|
|
if aud == clientID {
|
|
// the introspection response only has to return a boolean (active) if the token is active
|
|
// this will automatically be done by the library if you don't return an error
|
|
// you can also return further information about the user / associated token
|
|
// e.g. the userinfo (equivalent to userinfo endpoint)
|
|
err := s.setUserinfo(ctx, introspection, subject, clientID, token.Scopes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
//...and also the requested scopes...
|
|
introspection.SetScopes(token.Scopes)
|
|
//...and the client the token was issued to
|
|
introspection.SetClientID(token.ApplicationID)
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("token is not valid for this client")
|
|
}
|
|
|
|
// GetPrivateClaimsFromScopes implements the op.Storage interface
|
|
// it will be called for the creation of a JWT access token to assert claims for custom scopes
|
|
func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) {
|
|
for _, scope := range scopes {
|
|
switch scope {
|
|
case CustomScope:
|
|
claims = appendClaim(claims, CustomClaim, customClaim(clientID))
|
|
}
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// GetKeyByIDAndUserID implements the op.Storage interface
|
|
// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication)
|
|
func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
service, ok := s.services[clientID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("clientID not found")
|
|
}
|
|
key, ok := service.keys[keyID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("key not found")
|
|
}
|
|
return &jose.JSONWebKey{
|
|
KeyID: keyID,
|
|
Use: "sig",
|
|
Key: key,
|
|
}, nil
|
|
}
|
|
|
|
// ValidateJWTProfileScopes implements the op.Storage interface
|
|
// it will be called to validate the scopes of a JWT Profile Authorization Grant request
|
|
func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) {
|
|
allowedScopes := make([]string, 0)
|
|
for _, scope := range scopes {
|
|
if scope == oidc.ScopeOpenID {
|
|
allowedScopes = append(allowedScopes, scope)
|
|
}
|
|
}
|
|
return allowedScopes, nil
|
|
}
|
|
|
|
// Health implements the op.Storage interface
|
|
func (s *Storage) Health(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// createRefreshToken will store a refresh_token in-memory based on the provided information
|
|
func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
token := &RefreshToken{
|
|
ID: accessToken.RefreshTokenID,
|
|
Token: accessToken.RefreshTokenID,
|
|
AuthTime: authTime,
|
|
AMR: amr,
|
|
ApplicationID: accessToken.ApplicationID,
|
|
UserID: accessToken.Subject,
|
|
Audience: accessToken.Audience,
|
|
Expiration: time.Now().Add(5 * time.Hour),
|
|
Scopes: accessToken.Scopes,
|
|
}
|
|
s.refreshTokens[token.ID] = token
|
|
return token.Token, nil
|
|
}
|
|
|
|
// renewRefreshToken checks the provided refresh_token and creates a new one based on the current
|
|
func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
refreshToken, ok := s.refreshTokens[currentRefreshToken]
|
|
if !ok {
|
|
return "", "", fmt.Errorf("invalid refresh token")
|
|
}
|
|
// deletes the refresh token and all access tokens which were issued based on this refresh token
|
|
delete(s.refreshTokens, currentRefreshToken)
|
|
for _, token := range s.tokens {
|
|
if token.RefreshTokenID == currentRefreshToken {
|
|
delete(s.tokens, token.ID)
|
|
break
|
|
}
|
|
}
|
|
// creates a new refresh token based on the current one
|
|
token := uuid.NewString()
|
|
refreshToken.Token = token
|
|
refreshToken.ID = token
|
|
s.refreshTokens[token] = refreshToken
|
|
return token, refreshToken.ID, nil
|
|
}
|
|
|
|
// accessToken will store an access_token in-memory based on the provided information
|
|
func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
token := &Token{
|
|
ID: uuid.NewString(),
|
|
ApplicationID: applicationID,
|
|
RefreshTokenID: refreshTokenID,
|
|
Subject: subject,
|
|
Audience: audience,
|
|
Expiration: time.Now().Add(5 * time.Minute),
|
|
Scopes: scopes,
|
|
}
|
|
s.tokens[token.ID] = token
|
|
return token, nil
|
|
}
|
|
|
|
// setUserinfo sets the info based on the user, scopes and if necessary the clientID
|
|
func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, clientID string, scopes []string) (err error) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
user := s.userStore.GetUserByID(userID)
|
|
if user == nil {
|
|
return fmt.Errorf("user not found")
|
|
}
|
|
for _, scope := range scopes {
|
|
switch scope {
|
|
case oidc.ScopeOpenID:
|
|
userInfo.SetSubject(user.ID)
|
|
case oidc.ScopeEmail:
|
|
userInfo.SetEmail(user.Email, user.EmailVerified)
|
|
case oidc.ScopeProfile:
|
|
userInfo.SetPreferredUsername(user.Username)
|
|
userInfo.SetName(user.FirstName + " " + user.LastName)
|
|
userInfo.SetFamilyName(user.LastName)
|
|
userInfo.SetGivenName(user.FirstName)
|
|
userInfo.SetLocale(user.PreferredLanguage)
|
|
case oidc.ScopePhone:
|
|
userInfo.SetPhone(user.Phone, user.PhoneVerified)
|
|
case CustomScope:
|
|
// you can also have a custom scope and assert public or custom claims based on that
|
|
userInfo.AppendClaims(CustomClaim, customClaim(clientID))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation
|
|
func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) {
|
|
authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access)
|
|
if ok {
|
|
return authReq.ApplicationID, authReq.authTime, authReq.GetAMR()
|
|
}
|
|
refreshReq, ok := req.(*RefreshTokenRequest) // Refresh Token Request
|
|
if ok {
|
|
return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR
|
|
}
|
|
return "", time.Time{}, nil
|
|
}
|
|
|
|
// customClaim demonstrates how to return custom claims based on provided information
|
|
func customClaim(clientID string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"client": clientID,
|
|
"other": "stuff",
|
|
}
|
|
}
|
|
|
|
func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} {
|
|
if claims == nil {
|
|
claims = make(map[string]interface{})
|
|
}
|
|
claims[claim] = value
|
|
return claims
|
|
}
|