Files
opencloud/vendor/github.com/libregraph/lico/oidc/payload/registration.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

316 lines
10 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 (
"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
}