Files
opencloud/vendor/github.com/libregraph/lico/oidc/payload/authentication.go
Christian Richter 16307e036d add new property IdentifierDefaultLogoTargetURI
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-04-28 13:36:13 +02:00

463 lines
15 KiB
Go

/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// AuthenticationRequest holds the incoming parameters and request data for
// the OpenID Connect 1.0 authorization endpoint as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
type AuthenticationRequest struct {
providerMetadata *oidc.WellKnown
RawScope string `schema:"scope"`
Claims *ClaimsRequest `schema:"claims"`
RawResponseType string `schema:"response_type"`
ResponseMode string `schema:"response_mode"`
ClientID string `schema:"client_id"`
RawRedirectURI string `schema:"redirect_uri"`
State string `schema:"state"`
Nonce string `schema:"nonce"`
RawPrompt string `schema:"prompt"`
RawIDTokenHint string `schema:"id_token_hint"`
RawMaxAge string `schema:"max_age"`
RawRequest string `schema:"request"`
RawRequestURI string `schema:"request_uri"`
RawRegistration string `schema:"registration"`
CodeChallenge string `schema:"code_challenge"`
CodeChallengeMethod string `schema:"code_challenge_method"`
Scopes map[string]bool `schema:"-"`
ResponseTypes map[string]bool `schema:"-"`
Prompts map[string]bool `schema:"-"`
RedirectURI *url.URL `schema:"-"`
IDTokenHint *jwt.Token `schema:"-"`
MaxAge time.Duration `schema:"-"`
Request *jwt.Token `schema:"-"`
UseFragment bool `schema:"-"`
Flow string `schema:"-"`
Session *Session `schema:"-"`
}
// DecodeAuthenticationRequest returns a AuthenticationRequest holding the
// provided requests form data.
func DecodeAuthenticationRequest(req *http.Request, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
return NewAuthenticationRequest(req.Form, providerMetadata, keyFunc)
}
// NewAuthenticationRequest returns a AuthenticationRequest holding the
// provided url values.
func NewAuthenticationRequest(values url.Values, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
ar := &AuthenticationRequest{
providerMetadata: providerMetadata,
Scopes: make(map[string]bool),
ResponseTypes: make(map[string]bool),
Prompts: make(map[string]bool),
}
err := DecodeSchema(ar, values)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication request: %v", err)
}
if ar.RawScope != "" {
// Parse scope early, since the value is needed to handle the request
// parameter properly.
for _, scope := range strings.Split(ar.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if ar.RawRequest != "" {
parser := &jwt.Parser{}
request, err := parser.ParseWithClaims(ar.RawRequest, &RequestObjectClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, err.Error())
}
if claims, ok := request.Claims.(*RequestObjectClaims); ok {
err = ar.ApplyRequestObject(claims, request.Method)
if err != nil {
return nil, err
}
}
ar.Request = request
}
ar.RedirectURI, _ = url.Parse(ar.RawRedirectURI)
if ar.RawResponseType != "" {
for _, rt := range strings.Split(ar.RawResponseType, " ") {
ar.ResponseTypes[rt] = true
}
}
if ar.RawPrompt != "" {
for _, prompt := range strings.Split(ar.RawPrompt, " ") {
ar.Prompts[prompt] = true
}
}
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
ar.Flow = oidc.FlowCode
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
ar.UseFragment = true
ar.Flow = oidc.FlowImplicit
case oidc.ResponseTypeCodeIDToken:
// Hybrid flow.
fallthrough
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
fallthrough
case oidc.ResponseTypeCodeIDTokenToken:
// Hybrid flow.
ar.UseFragment = true
ar.Flow = oidc.FlowHybrid
}
switch ar.ResponseMode {
case oidc.ResponseModeFragment:
ar.UseFragment = true
// breaks
case oidc.ResponseModeQuery:
ar.UseFragment = false
// breaks
}
if ar.RawMaxAge != "" {
maxAgeInt, err := strconv.ParseInt(ar.RawMaxAge, 10, 64)
if err != nil {
return nil, err
}
ar.MaxAge = time.Duration(maxAgeInt) * time.Second
}
if ar.Claims != nil && ar.Claims.Passthru != nil {
// Remove pass thru claims when not provided in a secure manner. This
// means that pass through claims can only be passed via a signed request
// objects and its claims.
if ar.Request == nil || ar.Request.Method == jwt.SigningMethodNone || ar.Request.Claims == nil {
ar.Claims.Passthru = nil
}
}
return ar, nil
}
// ApplyRequestObject applies the provided request object claims to the
// associated authentication request data with validation as required.
func (ar *AuthenticationRequest) ApplyRequestObject(roc *RequestObjectClaims, method jwt.SigningMethod) error {
// Basic consistency validation following spec at
// https://openid.net/specs/openid-connect-core-1_0.html#SignedRequestObject
if ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "openid scope required when using the request parameter")
}
if roc.RawScope != "" {
ar.Scopes = make(map[string]bool)
// Parse scope directly, since the accociated authentication request
// has already parsed it when this is called.
for _, scope := range strings.Split(roc.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if roc.RawResponseType != "" {
if roc.RawResponseType != ar.RawResponseType {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object response_type mismatch")
}
}
if roc.ClientID != "" {
if roc.ClientID != ar.ClientID {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object client_id mismatch")
}
}
if method != jwt.SigningMethodNone {
// Additional claim validation when signed. The spec says that iss and
// aud SHOULD have defined values. So for now we do not enforce here.
}
// Apply rest of the provided request object values to the accociated
// authentication request.
if roc.Claims != nil {
// NOTE(longsleep): Overwrite request claims with the signed claims
// from the request object. This ensures that only signed claims are
// processed if any have been given. If no signed claims have been
// given, the unsigned claims are kept, leaving it to further checks
// to ensure that only signed claims are used by checking that the
// roc object has claims.
ar.Claims = roc.Claims
}
if roc.RawRedirectURI != "" {
ar.RawRedirectURI = roc.RawRedirectURI
}
if roc.State != "" {
ar.State = roc.State
}
if roc.Nonce != "" {
ar.Nonce = roc.Nonce
}
if roc.RawPrompt != "" {
ar.RawPrompt = roc.RawPrompt
}
if roc.RawIDTokenHint != "" {
ar.RawIDTokenHint = roc.RawIDTokenHint
}
if roc.RawMaxAge != "" {
ar.RawMaxAge = roc.RawMaxAge
}
if roc.RawRegistration != "" {
ar.RawRegistration = roc.RawRegistration
}
if roc.CodeChallengeMethod != "" {
ar.CodeChallengeMethod = roc.CodeChallengeMethod
}
if roc.CodeChallenge != "" {
ar.CodeChallenge = roc.CodeChallenge
}
return nil
}
// Validate validates the request data of the accociated authentication request.
func (ar *AuthenticationRequest) Validate(keyFunc jwt.Keyfunc) error {
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
// breaks
case oidc.ResponseTypeCodeIDToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
// breaks
case oidc.ResponseTypeCodeIDTokenToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
if ar.Nonce == "" {
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "nonce is required for implicit flow")
}
case oidc.ResponseTypeToken:
// OAuth2 flow implicit grant.
// breaks
default:
return ar.NewError(oidc.ErrorCodeOAuth2UnsupportedResponseType, "")
}
// Additional checks for flows with code.
if ar.Flow == oidc.FlowCode || ar.Flow == oidc.FlowHybrid {
switch ar.CodeChallengeMethod {
case "":
// breaks
case oidc.S256CodeChallengeMethod:
// breaks
case oidc.PlainCodeChallengeMethod:
// Plain is discouraged, and thus not supported.
fallthrough
default:
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "transform algorithm not supported")
}
}
if _, hasNonePrompt := ar.Prompts[oidc.PromptNone]; hasNonePrompt {
if len(ar.Prompts) > 1 {
// Cannot have other prompts if none is requested.
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "cannot request other prompts together with none")
}
}
if ar.ClientID == "" {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing client_id")
}
// TODO(longsleep): implement client_id white list.
if ar.RedirectURI == nil || !ar.RedirectURI.IsAbs() {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "invalid or missing redirect_uri")
}
if ar.RawIDTokenHint != "" {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
idTokenHint, err := parser.ParseWithClaims(ar.RawIDTokenHint, &konnectoidc.IDTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
ar.IDTokenHint = idTokenHint
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if _, withCodeResponseType := ar.ResponseTypes[oidc.ResponseTypeCode]; !withCodeResponseType {
// Ignore the offline_access request unless the Client is using a
// response_type value that would result in an Authorization Code
// being returned.
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
if ar.RawRequestURI != "" {
return ar.NewError(oidc.ErrorCodeOIDCRequestURINotSupported, "")
}
if ar.RawRegistration != "" {
return ar.NewError(oidc.ErrorCodeOIDCRegistrationNotSupported, "")
}
return nil
}
// Verify checks that the passed parameters match the accociated requirements.
func (ar *AuthenticationRequest) Verify(userID string) error {
if ar.IDTokenHint != nil {
// Compare userID with IDTokenHint.
if userID != ar.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
return ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "userid mismatch")
}
}
return nil
}
// NewError creates a new error with id and string and the associated request's
// state.
func (ar *AuthenticationRequest) NewError(id string, description string) *AuthenticationError {
return &AuthenticationError{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// NewBadRequest creates a new error with id and string and the associated
// request's state.
func (ar *AuthenticationRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
return &AuthenticationBadRequest{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// AuthenticationSuccess holds the outgoind data for a successful OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse.
// https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
type AuthenticationSuccess struct {
Code string `url:"code,omitempty"`
AccessToken string `url:"access_token,omitempty"`
TokenType string `url:"token_type,omitempty"`
IDToken string `url:"id_token,omitempty"`
State string `url:"state"`
ExpiresIn int64 `url:"expires_in,omitempty"`
Scope string `url:"scope,omitempty"`
SessionState string `url:"session_state,omitempty"`
}
// AuthenticationError holds the outgoind data for a failed OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthError and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError.
type AuthenticationError struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationError) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationError) Description() string {
return ae.ErrorDescription
}
// AuthenticationBadRequest holds the outgoing data for a failed OpenID Connect
// 1.0 authorize request with bad request parameters which make it impossible to
// continue with normal auth.
type AuthenticationBadRequest struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationBadRequest) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationBadRequest) Description() string {
return ae.ErrorDescription
}