mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 05:01:10 -05:00
316 lines
10 KiB
Go
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
|
|
}
|