jwt profile and fixes

This commit is contained in:
Livio Amstutz 2022-04-06 08:49:08 +02:00
parent d91fe7aacf
commit 8b1d405f91
No known key found for this signature in database
GPG key ID: 26BB1C2FA5952CF0
5 changed files with 168 additions and 168 deletions

View file

@ -7,8 +7,125 @@ import (
"github.com/caos/oidc/pkg/op"
)
var clients = map[string]*Client{}
var (
//we use the default login UI and pass the (auth request) id
defaultLoginURL = func(id string) string {
return "/login/username?authRequestID=" + id
}
//clients to be used by the storage interface
clients = map[string]*Client{}
)
//Client represents the internal model of an OAuth/OIDC client
//this could also be your database model
type Client struct {
id string
secret string
redirectURIs []string
applicationType op.ApplicationType
authMethod oidc.AuthMethod
loginURL func(string) string
responseTypes []oidc.ResponseType
grantTypes []oidc.GrantType
accessTokenType op.AccessTokenType
devMode bool
idTokenUserinfoClaimsAssertion bool
clockSkew time.Duration
}
//GetID must return the client_id
func (c *Client) GetID() string {
return c.id
}
//RedirectURIs must return the registered redirect_uris for Code and Implicit Flow
func (c *Client) RedirectURIs() []string {
return c.redirectURIs
}
//PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for signouts
func (c *Client) PostLogoutRedirectURIs() []string {
return []string{}
}
//ApplicationType must return the type of the client (app, native, user agent)
func (c *Client) ApplicationType() op.ApplicationType {
return c.applicationType
}
//AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt)
func (c *Client) AuthMethod() oidc.AuthMethod {
return c.authMethod
}
//ResponseTypes must return all allowed response types (code, id_token token, id_token)
//these must match with the allowed grant types
func (c *Client) ResponseTypes() []oidc.ResponseType {
return c.responseTypes
}
//GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer)
func (c *Client) GrantTypes() []oidc.GrantType {
return c.grantTypes
}
//LoginURL will be called to redirect the user (agent) to the login UI
//you could implement some logic here to redirect the users to different login UIs depending on the client
func (c *Client) LoginURL(id string) string {
return c.loginURL(id)
}
//AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT)
func (c *Client) AccessTokenType() op.AccessTokenType {
return c.accessTokenType
}
//IDTokenLifetime must return the lifetime of the client's id_tokens
func (c *Client) IDTokenLifetime() time.Duration {
return 1 * time.Hour
}
//DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client)
func (c *Client) DevMode() bool {
return c.devMode
}
//RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token
func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
//RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token
func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
//IsScopeAllowed enables Client specific custom scopes validation
//in this example we allow the CustomScope for all clients
func (c *Client) IsScopeAllowed(scope string) bool {
return scope == CustomScope
}
//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)
//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, ...)
func (c *Client) ClockSkew() time.Duration {
return c.clockSkew
}
//RegisterClients enables you to register clients for the example
func RegisterClients(registerClients ...*Client) {
for _, client := range registerClients {
clients[client.id] = client
@ -33,7 +150,7 @@ func NativeClient(id string, redirectURIs ...string) *Client {
redirectURIs: redirectURIs,
applicationType: op.ApplicationTypeNative,
authMethod: oidc.AuthMethodNone,
defaultLoginURL: defaultLoginURL,
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
accessTokenType: 0,
@ -45,18 +162,21 @@ func NativeClient(id string, redirectURIs ...string) *Client {
//WebClient will create a client of type web, which will always use PKCE and allow the use of refresh tokens
//user-defined redirectURIs may include:
// - http://localhost without port specification (e.g. http://localhost/auth/callback)
// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback)
//(the example will be used as default, if none is provided)
func WebClient(id, secret string, redirectURIs ...string) *Client {
return &Client{
id: id,
secret: secret,
redirectURIs: []string{
if len(redirectURIs) == 0 {
redirectURIs = []string{
"http://localhost:9999/auth/callback",
},
}
}
return &Client{
id: id,
secret: secret,
redirectURIs: redirectURIs,
applicationType: op.ApplicationTypeWeb,
authMethod: oidc.AuthMethodBasic,
defaultLoginURL: defaultLoginURL,
loginURL: defaultLoginURL,
responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode},
grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken},
accessTokenType: 0,
@ -65,88 +185,3 @@ func WebClient(id, secret string, redirectURIs ...string) *Client {
clockSkew: 0,
}
}
type Client struct {
id string
secret string
redirectURIs []string
applicationType op.ApplicationType
authMethod oidc.AuthMethod
defaultLoginURL func(string) string
responseTypes []oidc.ResponseType
grantTypes []oidc.GrantType
accessTokenType op.AccessTokenType
devMode bool
idTokenUserinfoClaimsAssertion bool
clockSkew time.Duration
}
func (c *Client) GetID() string {
return c.id
}
func (c *Client) RedirectURIs() []string {
return c.redirectURIs
}
func (c *Client) PostLogoutRedirectURIs() []string {
return []string{}
}
func (c *Client) ApplicationType() op.ApplicationType {
return c.applicationType
}
func (c *Client) AuthMethod() oidc.AuthMethod {
return c.authMethod
}
func (c *Client) ResponseTypes() []oidc.ResponseType {
return c.responseTypes
}
func (c *Client) GrantTypes() []oidc.GrantType {
return c.grantTypes
}
func (c *Client) LoginURL(id string) string {
//we use the default login UI and pass the (auth request) id,
//but you could implement some logic here to redirect the users to different login UIs depending on the client
return c.defaultLoginURL(id)
}
func (c *Client) AccessTokenType() op.AccessTokenType {
return c.accessTokenType
}
func (c *Client) IDTokenLifetime() time.Duration {
return 1 * time.Hour
}
func (c *Client) DevMode() bool {
return c.devMode
}
func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
return func(scopes []string) []string {
return scopes
}
}
func (c *Client) IsScopeAllowed(scope string) bool {
return false
}
func (c *Client) IDTokenUserinfoClaimsAssertion() bool {
return c.idTokenUserinfoClaimsAssertion
}
func (c *Client) ClockSkew() time.Duration {
return c.clockSkew
}

View file

@ -25,13 +25,13 @@ type AuthRequest struct {
ApplicationID string
CallbackURI string
TransferState string
Prompt []Prompt
Prompt []string
UiLocales []language.Tag
LoginHint string
MaxAuthAge *time.Duration
UserID string
Scopes []string
ResponseType OIDCResponseType
ResponseType oidc.ResponseType
Nonce string
CodeChallenge *OIDCCodeChallenge
@ -80,7 +80,7 @@ func (a *AuthRequest) GetRedirectURI() string {
}
func (a *AuthRequest) GetResponseType() oidc.ResponseType {
return ResponseTypeToOIDC(a.ResponseType)
return a.ResponseType
}
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
@ -103,54 +103,20 @@ func (a *AuthRequest) Done() bool {
return a.passwordChecked //this example only uses password for authentication
}
type Prompt int32
const (
PromptUnspecified Prompt = iota
PromptNone
PromptLogin
PromptConsent
PromptSelectAccount
)
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []Prompt {
prompts := make([]Prompt, len(oidcPrompt))
func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string {
prompts := make([]string, len(oidcPrompt))
for _, oidcPrompt := range oidcPrompt {
switch oidcPrompt {
case oidc.PromptNone:
prompts = append(prompts, PromptNone)
case oidc.PromptLogin:
prompts = append(prompts, PromptLogin)
case oidc.PromptConsent:
prompts = append(prompts, PromptConsent)
case oidc.PromptSelectAccount:
prompts = append(prompts, PromptSelectAccount)
case oidc.PromptNone,
oidc.PromptLogin,
oidc.PromptConsent,
oidc.PromptSelectAccount:
prompts = append(prompts, oidcPrompt)
}
}
return prompts
}
type OIDCResponseType int32
const (
OIDCResponseTypeCode OIDCResponseType = iota
OIDCResponseTypeIDToken
OIDCResponseTypeIDTokenToken
)
func ResponseTypeToInternal(responseType oidc.ResponseType) OIDCResponseType {
switch responseType {
case oidc.ResponseTypeCode:
return OIDCResponseTypeCode
case oidc.ResponseTypeIDTokenOnly:
return OIDCResponseTypeIDToken
case oidc.ResponseTypeIDToken:
return OIDCResponseTypeIDTokenToken
default:
return OIDCResponseTypeCode
}
}
func MaxAgeToInternal(maxAge *uint) *time.Duration {
if maxAge == nil {
return nil
@ -159,13 +125,6 @@ func MaxAgeToInternal(maxAge *uint) *time.Duration {
return &dur
}
type AuthRequestOIDC struct {
Scopes []string
ResponseType interface{}
Nonce string
CodeChallenge *OIDCCodeChallenge
}
func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest {
return &AuthRequest{
CreationDate: time.Now(),
@ -178,7 +137,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
MaxAuthAge: MaxAgeToInternal(authReq.MaxAge),
UserID: userID,
Scopes: authReq.Scopes,
ResponseType: ResponseTypeToInternal(authReq.ResponseType),
ResponseType: authReq.ResponseType,
Nonce: authReq.Nonce,
CodeChallenge: &OIDCCodeChallenge{
Challenge: authReq.CodeChallenge,
@ -206,19 +165,6 @@ func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge {
}
}
func ResponseTypeToOIDC(responseType OIDCResponseType) oidc.ResponseType {
switch responseType {
case OIDCResponseTypeCode:
return oidc.ResponseTypeCode
case OIDCResponseTypeIDTokenToken:
return oidc.ResponseTypeIDToken
case OIDCResponseTypeIDToken:
return oidc.ResponseTypeIDTokenOnly
default:
return oidc.ResponseTypeCode
}
}
//RefreshTokenRequestFromBusiness will simply wrap the internal RefreshToken to implement the op.RefreshTokenRequest interface
func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest {
return &RefreshTokenRequest{token}

View file

@ -23,6 +23,7 @@ type storage struct {
tokens map[string]*Token
clients map[string]*Client
users map[string]*User
services map[string]Service
refreshTokens map[string]*RefreshToken
signingKey signingKey
}
@ -33,11 +34,6 @@ type signingKey struct {
Key *rsa.PrivateKey
}
//TODO: describe
var defaultLoginURL = func(id string) string {
return "/login/username?authRequestID=" + id
}
func NewStorage() *storage {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
return &storage{
@ -378,13 +374,28 @@ func (s *storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, client
//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, userID string) (*jose.JSONWebKey, error) {
return nil, fmt.Errorf("example does not yet support authentication with JWT") //TODO: implement
service, ok := s.services[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
key, ok := service.keys[keyID]
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) {
return nil, fmt.Errorf("example does not yet support authentication with JWT") //TODO: implement
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

View file

@ -1,6 +1,10 @@
package internal
import "golang.org/x/text/language"
import (
"crypto/rsa"
"golang.org/x/text/language"
)
type User struct {
id string
@ -14,3 +18,7 @@ type User struct {
phoneVerified bool
preferredLanguage language.Tag
}
type Service struct {
keys map[string]*rsa.PublicKey
}

View file

@ -99,7 +99,7 @@ func newOP(ctx context.Context, storage op.Storage, port string, key [32]byte) (
AuthMethodPost: true,
//enables additional authentication by using private_key_jwt
AuthMethodPrivateKeyJWT: false, //TODO: implement and set to true
AuthMethodPrivateKeyJWT: true,
//enables refresh_token grant use
GrantTypeRefreshToken: true,