Initial QSfera import
This commit is contained in:
+462
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/libregraph/oidc-go"
|
||||
)
|
||||
|
||||
var scopedClaims = map[string]string{
|
||||
oidc.NameClaim: oidc.ScopeProfile,
|
||||
oidc.FamilyNameClaim: oidc.ScopeProfile,
|
||||
oidc.GivenNameClaim: oidc.ScopeProfile,
|
||||
oidc.MiddleNameClaim: oidc.ScopeProfile,
|
||||
oidc.PreferredUsernameClaim: oidc.ScopeProfile,
|
||||
oidc.ProfileClaim: oidc.ScopeProfile,
|
||||
oidc.PictureClaim: oidc.ScopeProfile,
|
||||
oidc.WebsiteClaim: oidc.ScopeProfile,
|
||||
oidc.GenderClaim: oidc.ScopeProfile,
|
||||
oidc.BirthdateClaim: oidc.ScopeProfile,
|
||||
oidc.ZoneinfoClaim: oidc.ScopeProfile,
|
||||
oidc.UpdatedAtClaim: oidc.ScopeProfile,
|
||||
|
||||
oidc.EmailClaim: oidc.ScopeEmail,
|
||||
oidc.EmailVerifiedClaim: oidc.ScopeEmail,
|
||||
}
|
||||
|
||||
// GetScopeForClaim returns the known scope if any for the provided claim name.
|
||||
func GetScopeForClaim(claim string) (string, bool) {
|
||||
scope, ok := scopedClaims[claim]
|
||||
return scope, ok
|
||||
}
|
||||
|
||||
// ClaimsRequest define the base claims structure for OpenID Connect claims
|
||||
// request parameter value as specified at
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - in
|
||||
// addition a Konnect specific pass thru value can be used to pass through any
|
||||
// application specific values to access and reqfresh tokens.
|
||||
type ClaimsRequest struct {
|
||||
UserInfo *ClaimsRequestMap `json:"userinfo,omitempty"`
|
||||
IDToken *ClaimsRequestMap `json:"id_token,omitempty"`
|
||||
Passthru json.RawMessage `json:"passthru,omitempty"`
|
||||
}
|
||||
|
||||
// ApplyScopes removes all claims requests from the accociated claims request
|
||||
// which are not mapped to one of the provided approved scopes.
|
||||
func (cr *ClaimsRequest) ApplyScopes(approvedScopes map[string]bool) error {
|
||||
if cr.UserInfo != nil {
|
||||
for claim := range *cr.UserInfo {
|
||||
if approved := approvedScopes[scopedClaims[claim]]; !approved {
|
||||
delete(*cr.UserInfo, claim)
|
||||
}
|
||||
}
|
||||
}
|
||||
if cr.IDToken != nil {
|
||||
for claim := range *cr.IDToken {
|
||||
if approved := approvedScopes[scopedClaims[claim]]; !approved {
|
||||
delete(*cr.IDToken, claim)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scopes adds all scopes of the accociated claims requests claims to
|
||||
// the provied scopes mapping safe the scopes already defined in the provided
|
||||
// excluded scopes mapping.
|
||||
func (cr *ClaimsRequest) Scopes(excludedScopes map[string]bool) []string {
|
||||
scopesMap := make(map[string]bool)
|
||||
|
||||
if cr.UserInfo != nil {
|
||||
for claim := range *cr.UserInfo {
|
||||
scope := scopedClaims[claim]
|
||||
if _, excluded := excludedScopes[scope]; !excluded {
|
||||
scopesMap[scope] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if cr.IDToken != nil {
|
||||
for claim := range *cr.IDToken {
|
||||
scope := scopedClaims[claim]
|
||||
if _, excluded := excludedScopes[scope]; !excluded {
|
||||
scopesMap[scope] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopes := make([]string, 0)
|
||||
for scope := range scopesMap {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
|
||||
return scopes
|
||||
}
|
||||
|
||||
// ClaimsRequestMap defines a mapping of claims request values used with
|
||||
// OpenID Connect claims request parameter values.
|
||||
type ClaimsRequestMap map[string]*ClaimsRequestValue
|
||||
|
||||
// ScopesMap returns a map of scopes defined by the claims in tha associated map.
|
||||
func (crm *ClaimsRequestMap) ScopesMap(excludedScopes map[string]bool) map[string]bool {
|
||||
scopesMap := make(map[string]bool)
|
||||
|
||||
for claim := range *crm {
|
||||
scope := scopedClaims[claim]
|
||||
if _, excluded := excludedScopes[scope]; !excluded {
|
||||
scopesMap[scope] = true
|
||||
}
|
||||
}
|
||||
|
||||
return scopesMap
|
||||
}
|
||||
|
||||
// Get returns the accociated maps claim value identified by the provided name.
|
||||
func (crm ClaimsRequestMap) Get(claim string) (*ClaimsRequestValue, bool) {
|
||||
value, ok := crm[claim]
|
||||
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// GetStringValue returns the accociated maps claim value identified by the
|
||||
// provided name as string value.
|
||||
func (crm ClaimsRequestMap) GetStringValue(claim string) (string, bool) {
|
||||
value, ok := crm.Get(claim)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
s, ok := value.Value.(string)
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// ClaimsRequestValue is the claims request detail definition of an OpenID
|
||||
// Connect claims request parameter value.
|
||||
type ClaimsRequestValue struct {
|
||||
Essential bool `json:"essential,omitempty"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Values []interface{} `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// Match returns true of the provided value is contained inside the accociated
|
||||
// request values values or value.
|
||||
func (crv *ClaimsRequestValue) Match(value interface{}) bool {
|
||||
if len(crv.Values) == 0 {
|
||||
return value == crv.Value
|
||||
}
|
||||
for _, v := range crv.Values {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ScopesValue is a string array with JSON marshal to/from a space separated
|
||||
// single string value.
|
||||
type ScopesValue []string
|
||||
|
||||
func (sv ScopesValue) MarshalJSON() ([]byte, error) {
|
||||
result := strings.Join(sv, " ")
|
||||
return json.Marshal(&result)
|
||||
}
|
||||
|
||||
func (sv *ScopesValue) UnmarshalJSON(data []byte) error {
|
||||
var parsed string
|
||||
err := json.Unmarshal(data, &parsed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := ScopesValue(strings.Split(parsed, " "))
|
||||
*sv = result
|
||||
return nil
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libregraph/oidc-go"
|
||||
|
||||
konnectoidc "github.com/libregraph/lico/oidc"
|
||||
)
|
||||
|
||||
// EndSessionRequest holds the incoming parameters and request data for OpenID
|
||||
// Connect Session Management 1.0 RP initiaed logout requests as specified at
|
||||
// https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
|
||||
type EndSessionRequest struct {
|
||||
providerMetadata *oidc.WellKnown
|
||||
|
||||
RawIDTokenHint string `schema:"id_token_hint"`
|
||||
RawPostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
|
||||
State string `schema:"state"`
|
||||
|
||||
IDTokenHint *jwt.Token `schema:"-"`
|
||||
PostLogoutRedirectURI *url.URL `schema:"-"`
|
||||
}
|
||||
|
||||
// DecodeEndSessionRequest returns a EndSessionRequest holding the
|
||||
// provided requests form data.
|
||||
func DecodeEndSessionRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
|
||||
return NewEndSessionRequest(req.Form, providerMetadata)
|
||||
}
|
||||
|
||||
// NewEndSessionRequest returns a EndSessionRequest holding the
|
||||
// provided url values.
|
||||
func NewEndSessionRequest(values url.Values, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
|
||||
esr := &EndSessionRequest{
|
||||
providerMetadata: providerMetadata,
|
||||
}
|
||||
err := DecodeSchema(esr, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
esr.PostLogoutRedirectURI, _ = url.Parse(esr.RawPostLogoutRedirectURI)
|
||||
|
||||
return esr, nil
|
||||
}
|
||||
|
||||
// Validate validates the request data of the accociated endSession request.
|
||||
func (esr *EndSessionRequest) Validate(keyFunc jwt.Keyfunc) error {
|
||||
if esr.RawIDTokenHint != "" {
|
||||
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||
idTokenHint, err := parser.ParseWithClaims(esr.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 esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
|
||||
}
|
||||
esr.IDTokenHint = idTokenHint
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify checks that the passed parameters match the accociated requirements.
|
||||
func (esr *EndSessionRequest) Verify(userID string) error {
|
||||
if esr.IDTokenHint != nil {
|
||||
// Compare userID with IDTokenHint.
|
||||
if userID != esr.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
|
||||
return esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "userid mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewError creates a new error with id and string and the associated request's
|
||||
// state.
|
||||
func (esr *EndSessionRequest) NewError(id string, description string) *AuthenticationError {
|
||||
return &AuthenticationError{
|
||||
ErrorID: id,
|
||||
ErrorDescription: description,
|
||||
State: esr.State,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBadRequest creates a new error with id and string and the associated
|
||||
// request's state.
|
||||
func (esr *EndSessionRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
|
||||
return &AuthenticationBadRequest{
|
||||
ErrorID: id,
|
||||
ErrorDescription: description,
|
||||
State: esr.State,
|
||||
}
|
||||
}
|
||||
|
||||
func (esr *EndSessionRequest) MakeRedirectEndSessionRequestURL() *url.URL {
|
||||
if esr.PostLogoutRedirectURI == nil || esr.PostLogoutRedirectURI.String() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if esr.State == "" {
|
||||
return esr.PostLogoutRedirectURI
|
||||
}
|
||||
uri, _ := url.Parse(esr.PostLogoutRedirectURI.String())
|
||||
query := uri.Query()
|
||||
query.Add("state", esr.State)
|
||||
uri.RawQuery = query.Encode()
|
||||
return uri
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libregraph/oidc-go"
|
||||
"github.com/mendsley/gojwk"
|
||||
|
||||
"github.com/libregraph/lico/identity/clients"
|
||||
konnectoidc "github.com/libregraph/lico/oidc"
|
||||
)
|
||||
|
||||
// ClientRegistrationRequest holds the incoming request data for the OpenID
|
||||
// Connect Dynamic Client Registration 1.0 client registration endpoint as
|
||||
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration and
|
||||
// https://openid.net/specs/openid-connect-session-1_0.html#DynRegRegistrations
|
||||
type ClientRegistrationRequest struct {
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ApplicationType string `json:"application_type"`
|
||||
|
||||
Contacts []string `json:"contacts"`
|
||||
ClientName string `json:"client_name"`
|
||||
ClientURI string `json:"client_uri"`
|
||||
|
||||
RawJWKS json.RawMessage `json:"jwks"`
|
||||
|
||||
RawIDTokenSignedResponseAlg string `json:"id_token_signed_response_alg"`
|
||||
RawUserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
|
||||
RawRequestObjectSigningAlg string `json:"request_object_signing_alg"`
|
||||
RawTokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||
RawTokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
|
||||
|
||||
PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris"`
|
||||
|
||||
JWKS *gojwk.Key `json:"-"`
|
||||
}
|
||||
|
||||
// DecodeClientRegistrationRequest returns a ClientRegistrationRequest holding
|
||||
// the provided request's data.
|
||||
func DecodeClientRegistrationRequest(req *http.Request) (*ClientRegistrationRequest, error) {
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "application/json") {
|
||||
return nil, fmt.Errorf("invalid content-type")
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
var crr ClientRegistrationRequest
|
||||
err := decoder.Decode(&crr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode client registration request: %v", err)
|
||||
}
|
||||
|
||||
if crr.RawJWKS != nil {
|
||||
jwks, err := gojwk.Unmarshal(crr.RawJWKS)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode client registration request jwks: %v", err)
|
||||
}
|
||||
// Only use keys.
|
||||
crr.JWKS = &gojwk.Key{
|
||||
Keys: jwks.Keys,
|
||||
}
|
||||
}
|
||||
|
||||
return &crr, err
|
||||
}
|
||||
|
||||
// Validate validates the request data of the accociated client registration
|
||||
// request and fills in default data where required.
|
||||
func (crr *ClientRegistrationRequest) Validate() error {
|
||||
if len(crr.RedirectURIs) == 0 {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "redirect_uris required")
|
||||
}
|
||||
|
||||
// Validate and filter response_type.
|
||||
if len(crr.ResponseTypes) == 0 {
|
||||
crr.ResponseTypes = []string{oidc.ResponseTypeCode}
|
||||
}
|
||||
requiredGrantTypes := make(map[string]bool)
|
||||
responseTypes := make([]string, 0)
|
||||
for _, responseType := range crr.ResponseTypes {
|
||||
switch responseType {
|
||||
case oidc.ResponseTypeCode:
|
||||
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
|
||||
responseTypes = append(responseTypes, responseType)
|
||||
// breaks
|
||||
|
||||
case oidc.ResponseTypeCodeIDToken:
|
||||
fallthrough
|
||||
case oidc.ResponseTypeCodeIDTokenToken:
|
||||
fallthrough
|
||||
case oidc.ResponseTypeCodeToken:
|
||||
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
|
||||
requiredGrantTypes[oidc.GrantTypeImplicit] = true
|
||||
responseTypes = append(responseTypes, responseType)
|
||||
// breaks
|
||||
|
||||
case oidc.ResponseTypeIDToken:
|
||||
fallthrough
|
||||
case oidc.ResponseTypeIDTokenToken:
|
||||
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
|
||||
requiredGrantTypes[oidc.GrantTypeImplicit] = true
|
||||
responseTypes = append(responseTypes, responseType)
|
||||
// breaks
|
||||
|
||||
case oidc.ResponseTypeToken:
|
||||
responseTypes = append(responseTypes, responseType)
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
crr.ResponseTypes = responseTypes
|
||||
|
||||
// Filter and validate grant_types.
|
||||
if len(crr.GrantTypes) == 0 {
|
||||
crr.GrantTypes = []string{oidc.GrantTypeAuthorizationCode}
|
||||
}
|
||||
grantTypes := make([]string, 0)
|
||||
registeredGrantTypes := make(map[string]bool)
|
||||
for _, grantType := range crr.GrantTypes {
|
||||
switch grantType {
|
||||
case oidc.GrantTypeAuthorizationCode:
|
||||
fallthrough
|
||||
case oidc.GrantTypeImplicit:
|
||||
fallthrough
|
||||
case oidc.GrantTypeRefreshToken:
|
||||
registeredGrantTypes[grantType] = true
|
||||
grantTypes = append(grantTypes, grantType)
|
||||
default:
|
||||
}
|
||||
}
|
||||
for grantType := range requiredGrantTypes {
|
||||
if ok := registeredGrantTypes[grantType]; !ok {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "grant_types conflict with response_types")
|
||||
}
|
||||
}
|
||||
|
||||
if crr.ApplicationType == "" {
|
||||
crr.ApplicationType = oidc.ApplicationTypeWeb
|
||||
}
|
||||
switch crr.ApplicationType {
|
||||
case oidc.ApplicationTypeWeb:
|
||||
// Web Clients using the OAuth Implicit Grant Type MUST only register
|
||||
// URLs using the https scheme as redirect_uris; they MUST NOT use
|
||||
// localhost as the hostname.
|
||||
for _, uriString := range crr.RedirectURIs {
|
||||
uri, err := url.Parse(uriString)
|
||||
if err != nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
|
||||
}
|
||||
if ok := registeredGrantTypes[oidc.GrantTypeImplicit]; ok {
|
||||
if uri.Scheme != "https" {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must use https redirect_uris")
|
||||
}
|
||||
if clients.IsLocalNativeHostURI(uri) {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must not use localhost redirect_uris")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case oidc.ApplicationTypeNative:
|
||||
// Native Clients MUST only register redirect_uris using custom URI
|
||||
// schemes or URLs using the http: scheme with localhost as the hostname.
|
||||
for _, uriString := range crr.RedirectURIs {
|
||||
uri, err := url.Parse(uriString)
|
||||
if err != nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
|
||||
}
|
||||
|
||||
if !clients.IsLocalNativeHTTPURI(uri) {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "native clients must only use localhost redirect_uris with http")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown application_type")
|
||||
}
|
||||
|
||||
if crr.RawIDTokenSignedResponseAlg == "" {
|
||||
crr.RawIDTokenSignedResponseAlg = jwt.SigningMethodRS256.Alg()
|
||||
}
|
||||
if crr.RawIDTokenSignedResponseAlg != "" {
|
||||
alg := jwt.GetSigningMethod(crr.RawIDTokenSignedResponseAlg)
|
||||
if alg == nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown id_token_signed_response_alg")
|
||||
}
|
||||
}
|
||||
if crr.RawUserInfoSignedResponseAlg != "" {
|
||||
alg := jwt.GetSigningMethod(crr.RawUserInfoSignedResponseAlg)
|
||||
if alg == nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown userinfo_signed_response_alg")
|
||||
}
|
||||
}
|
||||
if crr.RawRequestObjectSigningAlg != "" {
|
||||
alg := jwt.GetSigningMethod(crr.RawRequestObjectSigningAlg)
|
||||
if alg == nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown request_object_signing_alg")
|
||||
}
|
||||
}
|
||||
if crr.RawTokenEndpointAuthMethod == "" {
|
||||
crr.RawTokenEndpointAuthMethod = oidc.AuthMethodClientSecretBasic
|
||||
}
|
||||
if crr.RawTokenEndpointAuthMethod != "" {
|
||||
switch crr.RawTokenEndpointAuthMethod {
|
||||
case oidc.AuthMethodClientSecretBasic:
|
||||
// breaks
|
||||
case oidc.AuthMethodNone:
|
||||
// breaks
|
||||
default:
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unsupported token_endpoint_auth_method")
|
||||
}
|
||||
}
|
||||
if crr.RawTokenEndpointAuthSigningAlg != "" {
|
||||
alg := jwt.GetSigningMethod(crr.RawTokenEndpointAuthSigningAlg)
|
||||
if alg == nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown token_endpoint_auth_signing_alg")
|
||||
}
|
||||
}
|
||||
|
||||
for _, uriString := range crr.PostLogoutRedirectURIs {
|
||||
_, err := url.Parse(uriString)
|
||||
if err != nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "failed to parse post_logout_redirect_uris")
|
||||
}
|
||||
}
|
||||
|
||||
if crr.JWKS != nil {
|
||||
if len(crr.JWKS.Keys) == 0 {
|
||||
crr.JWKS = nil
|
||||
} else {
|
||||
enc := false
|
||||
empty := true
|
||||
for _, key := range crr.JWKS.Keys {
|
||||
switch key.Use {
|
||||
case "":
|
||||
if enc {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
|
||||
}
|
||||
empty = true
|
||||
key.Use = "sig"
|
||||
case "enc":
|
||||
enc = true
|
||||
if empty {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClientRegistration returns new dynamic client registration data for the
|
||||
// accociated client registration request.
|
||||
func (crr *ClientRegistrationRequest) ClientRegistration() (*clients.ClientRegistration, error) {
|
||||
cr := &clients.ClientRegistration{
|
||||
Contacts: crr.Contacts,
|
||||
Name: crr.ClientName,
|
||||
URI: crr.ClientURI,
|
||||
GrantTypes: crr.GrantTypes,
|
||||
ApplicationType: crr.ApplicationType,
|
||||
|
||||
RedirectURIs: crr.RedirectURIs,
|
||||
|
||||
JWKS: crr.JWKS,
|
||||
|
||||
RawIDTokenSignedResponseAlg: crr.RawIDTokenSignedResponseAlg,
|
||||
RawUserInfoSignedResponseAlg: crr.RawUserInfoSignedResponseAlg,
|
||||
RawRequestObjectSigningAlg: crr.RawRequestObjectSigningAlg,
|
||||
RawTokenEndpointAuthMethod: crr.RawTokenEndpointAuthMethod,
|
||||
RawTokenEndpointAuthSigningAlg: crr.RawTokenEndpointAuthSigningAlg,
|
||||
|
||||
PostLogoutRedirectURIs: crr.PostLogoutRedirectURIs,
|
||||
}
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
// ClientRegistrationResponse holds the outgoing data for a successful OpenID
|
||||
// Connect Dynamic Client Registration 1.0 clientregistration request as
|
||||
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse
|
||||
type ClientRegistrationResponse struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret,omitempty"`
|
||||
|
||||
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
|
||||
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
|
||||
|
||||
// Include validated request data.
|
||||
ClientRegistrationRequest
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 (
|
||||
"errors"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"github.com/libregraph/lico/identity/clients"
|
||||
)
|
||||
|
||||
// RequestObjectClaims holds the incoming request object claims provided as
|
||||
// JWT via request parameter to OpenID Connect 1.0 authorization endpoint
|
||||
// requests specified at
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests
|
||||
type RequestObjectClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
RawScope string `json:"scope"`
|
||||
Claims *ClaimsRequest `json:"claims"`
|
||||
RawResponseType string `json:"response_type"`
|
||||
ResponseMode string `json:"response_mode"`
|
||||
ClientID string `json:"client_id"`
|
||||
RawRedirectURI string `json:"redirect_uri"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
RawPrompt string `json:"prompt"`
|
||||
RawIDTokenHint string `json:"id_token_hint"`
|
||||
RawMaxAge string `json:"max_age"`
|
||||
|
||||
RawRegistration string `json:"registration"`
|
||||
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeChallengeMethod string `json:"code_challenge_method"`
|
||||
|
||||
client *clients.Secured
|
||||
}
|
||||
|
||||
// SetSecure sets the provided client as owner of the accociated claims.
|
||||
func (roc *RequestObjectClaims) SetSecure(client *clients.Secured) error {
|
||||
if roc.ClientID != client.ID {
|
||||
return errors.New("client ID mismatch")
|
||||
}
|
||||
|
||||
roc.client = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Secure returns the accociated secure client or nil if not secure.
|
||||
func (roc *RequestObjectClaims) Secure() *clients.Secured {
|
||||
return roc.client
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
)
|
||||
|
||||
var decoder = schema.NewDecoder()
|
||||
var encoder = schema.NewEncoder()
|
||||
|
||||
// DecodeSchema decodes request form data into the provided dst schema struct.
|
||||
func DecodeSchema(dst interface{}, src map[string][]string) error {
|
||||
return decoder.Decode(dst, src)
|
||||
}
|
||||
|
||||
// EncodeSchema encodes the provided src schema to the provided map.
|
||||
func EncodeSchema(src interface{}, dst map[string][]string) error {
|
||||
return encoder.Encode(src, dst)
|
||||
}
|
||||
|
||||
// ConvertOIDCClaimsRequest is a converter function for oidc.ClaimsRequest data
|
||||
// provided in URL schema.
|
||||
func ConvertOIDCClaimsRequest(value string) reflect.Value {
|
||||
v := ClaimsRequest{}
|
||||
|
||||
if err := json.Unmarshal([]byte(value), &v); err != nil {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
|
||||
func init() {
|
||||
decoder.IgnoreUnknownKeys(true)
|
||||
decoder.RegisterConverter(ClaimsRequest{}, ConvertOIDCClaimsRequest)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
// Session defines a Provider's session with a String identifier for a Session.
|
||||
// This represents a Session of a User Agent or device for a logged-in End-User
|
||||
// at an RP. Different ID values are used to identify distinct sessions. This
|
||||
// is implemented as defined in the OIDC Front Channel logout extension
|
||||
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout
|
||||
type Session struct {
|
||||
Version int
|
||||
ID string
|
||||
Sub string
|
||||
Provider string
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/libregraph/oidc-go"
|
||||
|
||||
konnectoidc "github.com/libregraph/lico/oidc"
|
||||
)
|
||||
|
||||
// TokenRequest holds the incoming parameters and request data for
|
||||
// the OpenID Connect 1.0 token endpoint as specified at
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
||||
type TokenRequest struct {
|
||||
providerMetadata *oidc.WellKnown
|
||||
|
||||
GrantType string `schema:"grant_type"`
|
||||
Code string `schema:"code"`
|
||||
RawRedirectURI string `schema:"redirect_uri"`
|
||||
RawRefreshToken string `schema:"refresh_token"`
|
||||
RawScope string `schema:"scope"`
|
||||
|
||||
ClientID string `schema:"client_id"`
|
||||
ClientSecret string `schema:"client_secret"`
|
||||
|
||||
CodeVerifier string `schema:"code_verifier"`
|
||||
|
||||
RedirectURI *url.URL `schema:"-"`
|
||||
RefreshToken *jwt.Token `schema:"-"`
|
||||
Scopes map[string]bool `schema:"-"`
|
||||
}
|
||||
|
||||
// DecodeTokenRequest return a TokenRequest holding the provided
|
||||
// request's form data.
|
||||
func DecodeTokenRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
|
||||
tr, err := NewTokenRequest(req.PostForm, providerMetadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clientID string
|
||||
var clientSecret string
|
||||
|
||||
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
|
||||
switch auth[0] {
|
||||
case "Basic":
|
||||
// Support client_secret_basic authentication method.
|
||||
if len(auth) != 2 {
|
||||
return nil, fmt.Errorf("invalid Basic authorization header format")
|
||||
}
|
||||
var basic []byte
|
||||
if basic, err = base64.StdEncoding.DecodeString(auth[1]); err != nil {
|
||||
return nil, fmt.Errorf("invalid Basic authorization value: %w", err)
|
||||
}
|
||||
// Decode username as client ID and password as client secret. See
|
||||
// https://tools.ietf.org/html/rfc6749#section-2.3.1 for details.
|
||||
check := strings.SplitN(string(basic), ":", 2)
|
||||
if len(check) == 2 {
|
||||
// Data is encoded application/x-www-form-urlencoded UTF-8. See
|
||||
// https://tools.ietf.org/html/rfc6749#appendix-B for details.
|
||||
if clientID, err = url.QueryUnescape(check[0]); err == nil {
|
||||
clientSecret, _ = url.QueryUnescape(check[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tr.ClientID == "" {
|
||||
if clientID == "" {
|
||||
return nil, fmt.Errorf("client_id is missing")
|
||||
}
|
||||
// Use client ID and secret if no client_id was passed to the request directly.
|
||||
tr.ClientID = clientID
|
||||
tr.ClientSecret = clientSecret
|
||||
} else if clientID != "" {
|
||||
if tr.ClientID == clientID {
|
||||
// Update the client secret, if the ID is a match. This replaces
|
||||
// a directly given secret.
|
||||
tr.ClientSecret = clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
return tr, err
|
||||
}
|
||||
|
||||
// NewTokenRequest returns a TokenRequest holding the provided url values.
|
||||
func NewTokenRequest(values url.Values, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
|
||||
tr := &TokenRequest{
|
||||
providerMetadata: providerMetadata,
|
||||
|
||||
Scopes: make(map[string]bool),
|
||||
}
|
||||
|
||||
err := DecodeSchema(tr, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr.RedirectURI, _ = url.Parse(tr.RawRedirectURI)
|
||||
|
||||
if tr.RawScope != "" {
|
||||
for _, scope := range strings.Split(tr.RawScope, " ") {
|
||||
tr.Scopes[scope] = true
|
||||
}
|
||||
}
|
||||
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
// Validate validates the request data of the accociated token request.
|
||||
func (tr *TokenRequest) Validate(keyFunc jwt.Keyfunc, claims jwt.Claims) error {
|
||||
switch tr.GrantType {
|
||||
case oidc.GrantTypeAuthorizationCode:
|
||||
// breaks
|
||||
case oidc.GrantTypeRefreshToken:
|
||||
if tr.RawRefreshToken != "" {
|
||||
refreshToken, err := jwt.ParseWithClaims(tr.RawRefreshToken, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if keyFunc != nil {
|
||||
return keyFunc(token)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Not validated")
|
||||
})
|
||||
if err != nil {
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
|
||||
}
|
||||
tr.RefreshToken = refreshToken
|
||||
}
|
||||
// breaks
|
||||
|
||||
default:
|
||||
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2UnsupportedGrantType, "unsupported grant_type value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TokenSuccess holds the outgoing data for a successful OpenID
|
||||
// Connect 1.0 token request as specified at
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse.
|
||||
type TokenSuccess struct {
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 (
|
||||
"github.com/libregraph/lico/oidc"
|
||||
)
|
||||
|
||||
// UserInfoResponse defines the data returned from the OIDC UserInfo
|
||||
// endpoint.
|
||||
type UserInfoResponse struct {
|
||||
oidc.UserInfoClaims
|
||||
*oidc.ProfileClaims
|
||||
*oidc.EmailClaims
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// ToMap is a helper function to convert the provided payload struct to
|
||||
// a map type which can be used to extend the payload data with additional fields.
|
||||
func ToMap(payload interface{}) (map[string]interface{}, error) {
|
||||
// NOTE(longsleep): This implementation sucks, marshal to JSON and unmarshal
|
||||
// again - rly?
|
||||
intermediate, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims := make(map[string]interface{})
|
||||
err = json.Unmarshal(intermediate, &claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
Reference in New Issue
Block a user