Initial QSfera import
This commit is contained in:
+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
|
||||
}
|
||||
Reference in New Issue
Block a user