diff --git a/example/internal/mock/storage.go b/example/internal/mock/storage.go index b8a1648..214ba54 100644 --- a/example/internal/mock/storage.go +++ b/example/internal/mock/storage.go @@ -95,7 +95,7 @@ func (a *AuthRequest) GetScopes() []string { } } -func (a *AuthRequest) SetCurrentScopes(scopes oidc.Scopes) {} +func (a *AuthRequest) SetCurrentScopes(scopes []string) {} func (a *AuthRequest) GetState() string { return "" @@ -243,7 +243,7 @@ func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect return nil } -func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) { +func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope []string) ([]string, error) { return scope, nil } diff --git a/pkg/client/client.go b/pkg/client/client.go index b2b815e..708b79c 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -17,8 +17,8 @@ import ( var ( Encoder = func() utils.Encoder { e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() + e.RegisterEncoder(oidc.SpaceDelimitedArray{}, func(value reflect.Value) string { + return value.Interface().(oidc.SpaceDelimitedArray).Encode() }) return e }() diff --git a/pkg/client/rp/relaying_party.go b/pkg/client/rp/relaying_party.go index 5a48951..94593ff 100644 --- a/pkg/client/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -430,10 +430,10 @@ func WithCodeChallenge(codeChallenge string) AuthURLOpt { } //WithPrompt sets the `prompt` params in the auth request -func WithPrompt(prompt oidc.Prompt) AuthURLOpt { +func WithPrompt(prompt ...string) AuthURLOpt { return func() []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{ - oauth2.SetAuthURLParam("prompt", string(prompt)), + oauth2.SetAuthURLParam("prompt", oidc.SpaceDelimitedArray(prompt).Encode()), } } } diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 71776af..79d0c1e 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -44,39 +44,39 @@ const ( //PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. //An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed - PromptNone Prompt = "none" + PromptNone = "none" //PromptLogin (`login`) directs the Authorization Server to prompt the End-User for reauthentication. - PromptLogin Prompt = "login" + PromptLogin = "login" //PromptConsent (`consent`) directs the Authorization Server to prompt the End-User for consent (of sharing information). - PromptConsent Prompt = "consent" + PromptConsent = "consent" //PromptSelectAccount (`select_account `) directs the Authorization Server to prompt the End-User to select a user account (to enable multi user / session switching) - PromptSelectAccount Prompt = "select_account" + PromptSelectAccount = "select_account" ) //AuthRequest according to: //https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { ID string - Scopes Scopes `schema:"scope"` - ResponseType ResponseType `schema:"response_type"` - ClientID string `schema:"client_id"` - RedirectURI string `schema:"redirect_uri"` //TODO: type + Scopes SpaceDelimitedArray `schema:"scope"` + ResponseType ResponseType `schema:"response_type"` + ClientID string `schema:"client_id"` + RedirectURI string `schema:"redirect_uri"` //TODO: type State string `schema:"state"` // ResponseMode TODO: ? - Nonce string `schema:"nonce"` - Display Display `schema:"display"` - Prompt Prompt `schema:"prompt"` - MaxAge uint32 `schema:"max_age"` - UILocales Locales `schema:"ui_locales"` - IDTokenHint string `schema:"id_token_hint"` - LoginHint string `schema:"login_hint"` - ACRValues []string `schema:"acr_values"` + Nonce string `schema:"nonce"` + Display Display `schema:"display"` + Prompt SpaceDelimitedArray `schema:"prompt"` + MaxAge *uint `schema:"max_age"` + UILocales Locales `schema:"ui_locales"` + IDTokenHint string `schema:"id_token_hint"` + LoginHint string `schema:"login_hint"` + ACRValues []string `schema:"acr_values"` CodeChallenge string `schema:"code_challenge"` CodeChallengeMethod CodeChallengeMethod `schema:"code_challenge_method"` diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index a2176aa..9b2bad7 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -21,7 +21,7 @@ type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool - SetScopes(scopes Scopes) + SetScopes(scopes []string) SetClientID(id string) } @@ -30,10 +30,10 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Scope Scopes `json:"scope,omitempty"` - ClientID string `json:"client_id,omitempty"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope SpaceDelimitedArray `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` userInfoProfile userInfoEmail userInfoPhone @@ -46,7 +46,7 @@ func (u *introspectionResponse) IsActive() bool { return u.Active } -func (u *introspectionResponse) SetScopes(scope Scopes) { +func (u *introspectionResponse) SetScopes(scope []string) { u.Scope = scope } diff --git a/pkg/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go index 6969783..25b7caa 100644 --- a/pkg/oidc/jwt_profile.go +++ b/pkg/oidc/jwt_profile.go @@ -1,9 +1,9 @@ package oidc type JWTProfileGrantRequest struct { - Assertion string `schema:"assertion"` - Scope Scopes `schema:"scope"` - GrantType GrantType `schema:"grant_type"` + Assertion string `schema:"assertion"` + Scope SpaceDelimitedArray `schema:"scope"` + GrantType GrantType `schema:"grant_type"` } //NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant diff --git a/pkg/oidc/token_request.go b/pkg/oidc/token_request.go index 1136f8e..4899c3a 100644 --- a/pkg/oidc/token_request.go +++ b/pkg/oidc/token_request.go @@ -58,12 +58,12 @@ func (a *AccessTokenRequest) SetClientSecret(clientSecret string) { } type RefreshTokenRequest struct { - RefreshToken string `schema:"refresh_token"` - Scopes Scopes `schema:"scope"` - ClientID string `schema:"client_id"` - ClientSecret string `schema:"client_secret"` - ClientAssertion string `schema:"client_assertion"` - ClientAssertionType string `schema:"client_assertion_type"` + RefreshToken string `schema:"refresh_token"` + Scopes SpaceDelimitedArray `schema:"scope"` + ClientID string `schema:"client_id"` + ClientSecret string `schema:"client_secret"` + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` } func (a *RefreshTokenRequest) GrantType() GrantType { @@ -81,12 +81,12 @@ func (a *RefreshTokenRequest) SetClientSecret(clientSecret string) { } type JWTTokenRequest struct { - Issuer string `json:"iss"` - Subject string `json:"sub"` - Scopes Scopes `json:"-"` - Audience Audience `json:"aud"` - IssuedAt Time `json:"iat"` - ExpiresAt Time `json:"exp"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Scopes SpaceDelimitedArray `json:"-"` + Audience Audience `json:"aud"` + IssuedAt Time `json:"iat"` + ExpiresAt Time `json:"exp"` } //GetIssuer implements the Claims interface @@ -143,12 +143,12 @@ func (j *JWTTokenRequest) GetScopes() []string { } type TokenExchangeRequest struct { - subjectToken string `schema:"subject_token"` - subjectTokenType string `schema:"subject_token_type"` - actorToken string `schema:"actor_token"` - actorTokenType string `schema:"actor_token_type"` - resource []string `schema:"resource"` - audience Audience `schema:"audience"` - Scope Scopes `schema:"scope"` - requestedTokenType string `schema:"requested_token_type"` + subjectToken string `schema:"subject_token"` + subjectTokenType string `schema:"subject_token_type"` + actorToken string `schema:"actor_token"` + actorTokenType string `schema:"actor_token_type"` + resource []string `schema:"resource"` + audience Audience `schema:"audience"` + Scope SpaceDelimitedArray `schema:"scope"` + requestedTokenType string `schema:"requested_token_type"` } diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 5525923..e72d67c 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -54,30 +54,36 @@ func (l *Locales) UnmarshalText(text []byte) error { return nil } -type Prompt string +type MaxAge *uint + +func NewMaxAge(i uint) MaxAge { + return &i +} + +type SpaceDelimitedArray []string + +type Prompt SpaceDelimitedArray type ResponseType string -type Scopes []string - -func (s Scopes) Encode() string { +func (s SpaceDelimitedArray) Encode() string { return strings.Join(s, " ") } -func (s *Scopes) UnmarshalText(text []byte) error { +func (s *SpaceDelimitedArray) UnmarshalText(text []byte) error { *s = strings.Split(string(text), " ") return nil } -func (s *Scopes) MarshalText() ([]byte, error) { +func (s SpaceDelimitedArray) MarshalText() ([]byte, error) { return []byte(s.Encode()), nil } -func (s *Scopes) MarshalJSON() ([]byte, error) { - return json.Marshal((*s).Encode()) +func (s SpaceDelimitedArray) MarshalJSON() ([]byte, error) { + return json.Marshal((s).Encode()) } -func (s *Scopes) UnmarshalJSON(data []byte) error { +func (s *SpaceDelimitedArray) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { return err diff --git a/pkg/oidc/types_test.go b/pkg/oidc/types_test.go index 8138b4b..c03a775 100644 --- a/pkg/oidc/types_test.go +++ b/pkg/oidc/types_test.go @@ -220,7 +220,7 @@ func TestScopes_UnmarshalText(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var scopes Scopes + var scopes SpaceDelimitedArray if err := scopes.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr { t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr) } @@ -230,7 +230,7 @@ func TestScopes_UnmarshalText(t *testing.T) { } func TestScopes_MarshalText(t *testing.T) { type args struct { - scopes Scopes + scopes SpaceDelimitedArray } type res struct { scopes []byte @@ -244,7 +244,7 @@ func TestScopes_MarshalText(t *testing.T) { { "unknown value", args{ - Scopes{"unknown"}, + SpaceDelimitedArray{"unknown"}, }, res{ []byte("unknown"), @@ -254,7 +254,7 @@ func TestScopes_MarshalText(t *testing.T) { { "struct", args{ - Scopes{`{"unknown":"value"}`}, + SpaceDelimitedArray{`{"unknown":"value"}`}, }, res{ []byte(`{"unknown":"value"}`), @@ -264,7 +264,7 @@ func TestScopes_MarshalText(t *testing.T) { { "openid", args{ - Scopes{"openid"}, + SpaceDelimitedArray{"openid"}, }, res{ []byte("openid"), @@ -274,7 +274,7 @@ func TestScopes_MarshalText(t *testing.T) { { "multiple scopes", args{ - Scopes{"openid", "email", "custom:scope"}, + SpaceDelimitedArray{"openid", "email", "custom:scope"}, }, res{ []byte("openid email custom:scope"), diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 9e0cd45..fce681f 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -106,7 +106,11 @@ func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRe } //ValidateAuthRequest validates the authorize parameters and returns the userID of the id_token_hint if passed -func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (string, error) { +func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier IDTokenHintVerifier) (sub string, err error) { + authReq.MaxAge, err = ValidateAuthReqPrompt(authReq.Prompt, authReq.MaxAge) + if err != nil { + return "", err + } client, err := storage.GetClientByClientID(ctx, authReq.ClientID) if err != nil { return "", ErrServerError(err.Error()) @@ -124,6 +128,19 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier) } +//ValidateAuthReqPrompt validates the passed prompt values and sets max_age to 0 if prompt login is present +func ValidateAuthReqPrompt(prompts []string, maxAge *uint) (_ *uint, err error) { + for _, prompt := range prompts { + if prompt == oidc.PromptNone && len(prompts) > 1 { + return nil, ErrInvalidRequest("The prompt parameter `none` must only be used as a single value") + } + if prompt == oidc.PromptLogin { + maxAge = oidc.NewMaxAge(0) + } + } + return maxAge, nil +} + //ValidateAuthReqScopes validates the passed scopes func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) { if len(scopes) == 0 { diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 9bec1e7..40e1a8a 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -123,7 +123,7 @@ func TestParseAuthorizeRequest(t *testing.T) { }(), }, res{ - &oidc.AuthRequest{Scopes: oidc.Scopes{"openid"}}, + &oidc.AuthRequest{Scopes: oidc.SpaceDelimitedArray{"openid"}}, false, }, }, diff --git a/pkg/op/mock/storage.mock.go b/pkg/op/mock/storage.mock.go index be261bb..4b44f2b 100644 --- a/pkg/op/mock/storage.mock.go +++ b/pkg/op/mock/storage.mock.go @@ -316,10 +316,10 @@ func (mr *MockStorageMockRecorder) TokenRequestByRefreshToken(arg0, arg1 interfa } // ValidateJWTProfileScopes mocks base method. -func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 oidc.Scopes) (oidc.Scopes, error) { +func (m *MockStorage) ValidateJWTProfileScopes(arg0 context.Context, arg1 string, arg2 []string) ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateJWTProfileScopes", arg0, arg1, arg2) - ret0, _ := ret[0].(oidc.Scopes) + ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/op/mock/storage.mock.impl.go b/pkg/op/mock/storage.mock.impl.go index 7689bd3..4855cf5 100644 --- a/pkg/op/mock/storage.mock.impl.go +++ b/pkg/op/mock/storage.mock.impl.go @@ -140,10 +140,10 @@ func (c *ConfClient) GetID() string { } func (c *ConfClient) AccessTokenLifetime() time.Duration { - return time.Duration(5 * time.Minute) + return 5 * time.Minute } func (c *ConfClient) IDTokenLifetime() time.Duration { - return time.Duration(5 * time.Minute) + return 5 * time.Minute } func (c *ConfClient) AccessTokenType() op.AccessTokenType { return c.accessTokenType diff --git a/pkg/op/session.go b/pkg/op/session.go index 19ebab4..4d75098 100644 --- a/pkg/op/session.go +++ b/pkg/op/session.go @@ -83,13 +83,3 @@ func ValidateEndSessionRequest(ctx context.Context, req *oidc.EndSessionRequest, } return nil, ErrInvalidRequest("post_logout_redirect_uri invalid") } - -func NeedsExistingSession(authRequest *oidc.AuthRequest) bool { - if authRequest == nil { - return true - } - if authRequest.Prompt == oidc.PromptNone { - return true - } - return false -} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 0e0794e..ca9ae7c 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -34,7 +34,7 @@ type OPStorage interface { SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) - ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) + ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) } type Storage interface { diff --git a/pkg/op/token_refresh.go b/pkg/op/token_refresh.go index 8072f30..debcca1 100644 --- a/pkg/op/token_refresh.go +++ b/pkg/op/token_refresh.go @@ -17,7 +17,7 @@ type RefreshTokenRequest interface { GetClientID() string GetScopes() []string GetSubject() string - SetCurrentScopes(scopes oidc.Scopes) + SetCurrentScopes(scopes []string) } //RefreshTokenExchange handles the OAuth 2.0 refresh_token grant, including @@ -72,7 +72,7 @@ func ValidateRefreshTokenRequest(ctx context.Context, tokenReq *oidc.RefreshToke //ValidateRefreshTokenScopes validates that the requested scope is a subset of the original auth request scope //it will set the requested scopes as current scopes onto RefreshTokenRequest //if empty the original scopes will be used -func ValidateRefreshTokenScopes(requestedScopes oidc.Scopes, authRequest RefreshTokenRequest) error { +func ValidateRefreshTokenScopes(requestedScopes []string, authRequest RefreshTokenRequest) error { if len(requestedScopes) == 0 { return nil }