mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-02 19:20:08 -05:00
463 lines
15 KiB
Go
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
|
|
}
|