Files
casdoor/object/wellknown_oidc_discovery.go

285 lines
10 KiB
Go

// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// 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 object
import (
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"strings"
"github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
"github.com/go-jose/go-jose/v4"
)
type OidcDiscovery struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
JwksUri string `json:"jwks_uri"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
RequestParameterSupported bool `json:"request_parameter_supported"`
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
EndSessionEndpoint string `json:"end_session_endpoint"`
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"` // RFC 9449
}
type WebFinger struct {
Subject string `json:"subject"`
Links []WebFingerLink `json:"links"`
Aliases *[]string `json:"aliases,omitempty"`
Properties *map[string]string `json:"properties,omitempty"`
}
type WebFingerLink struct {
Rel string `json:"rel"`
Href string `json:"href"`
}
func isIpAddress(host string) bool {
// Attempt to split the host and port, ignoring the error
hostWithoutPort, _, err := net.SplitHostPort(host)
if err != nil {
// If an error occurs, it might be because there's no port
// In that case, use the original host string
hostWithoutPort = host
}
// Attempt to parse the host as an IP address (both IPv4 and IPv6)
ip := net.ParseIP(hostWithoutPort)
// if host is not nil is an IP address else is not an IP address
return ip != nil
}
func getOriginFromHostInternal(host string) (string, string) {
origin := conf.GetConfigString("origin")
if origin != "" {
return origin, origin
}
isDev := conf.GetConfigString("runmode") == "dev"
// "door.casdoor.com"
protocol := "https://"
if !strings.Contains(host, ".") {
// "localhost:8000" or "computer-name:80"
protocol = "http://"
} else if isIpAddress(host) {
// "192.168.0.10"
protocol = "http://"
}
if host == "localhost:8000" && isDev {
return fmt.Sprintf("%s%s", protocol, "localhost:7001"), fmt.Sprintf("%s%s", protocol, "localhost:8000")
} else {
return fmt.Sprintf("%s%s", protocol, host), fmt.Sprintf("%s%s", protocol, host)
}
}
func getOriginFromHost(host string) (string, string) {
originF, originB := getOriginFromHostInternal(host)
originFrontend := conf.GetConfigString("originFrontend")
if originFrontend != "" {
originF = originFrontend
}
return originF, originB
}
func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
originFrontend, originBackend := getOriginFromHost(host)
// If application is provided, use application-specific URLs
var issuer, jwksUri string
if applicationName != "" {
// Application-specific issuer and endpoints (owner is always "admin")
issuer = fmt.Sprintf("%s/.well-known/%s", originBackend, applicationName)
jwksUri = fmt.Sprintf("%s/.well-known/%s/jwks", originBackend, applicationName)
} else {
// Default global issuer and endpoints
issuer = originBackend
jwksUri = fmt.Sprintf("%s/.well-known/jwks", originBackend)
}
// Default OIDC scopes
scopes := []string{"openid", "email", "profile", "address", "phone", "offline_access"}
// Merge application-specific custom scopes if application is provided
if applicationName != "" {
applicationId := util.GetId("admin", applicationName)
application, err := GetApplication(applicationId)
if err == nil && application != nil && len(application.Scopes) > 0 {
for _, scope := range application.Scopes {
// Add custom scope names to the scopes list
if scope.Name != "" {
scopes = append(scopes, scope.Name)
}
}
}
}
// Examples:
// https://login.okta.com/.well-known/openid-configuration
// https://auth0.auth0.com/.well-known/openid-configuration
// https://accounts.google.com/.well-known/openid-configuration
// https://access.line.me/.well-known/openid-configuration
oidcDiscovery := OidcDiscovery{
Issuer: issuer,
AuthorizationEndpoint: fmt.Sprintf("%s/login/oauth/authorize", originFrontend),
TokenEndpoint: fmt.Sprintf("%s/api/login/oauth/access_token", originBackend),
UserinfoEndpoint: fmt.Sprintf("%s/api/userinfo", originBackend),
DeviceAuthorizationEndpoint: fmt.Sprintf("%s/api/device-auth", originBackend),
RegistrationEndpoint: fmt.Sprintf("%s/api/oauth/register", originBackend),
JwksUri: jwksUri,
IntrospectionEndpoint: fmt.Sprintf("%s/api/login/oauth/introspect", originBackend),
ResponseTypesSupported: []string{"code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none"},
ResponseModesSupported: []string{"query", "fragment", "form_post"},
GrantTypesSupported: []string{"authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
SubjectTypesSupported: []string{"public"},
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
ScopesSupported: scopes,
CodeChallengeMethodsSupported: []string{"S256"},
ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"},
RequestParameterSupported: true,
RequestObjectSigningAlgValuesSupported: []string{"HS256", "HS384", "HS512"},
EndSessionEndpoint: fmt.Sprintf("%s/api/logout", originBackend),
DPoPSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"},
}
return oidcDiscovery
}
func GetJsonWebKeySet(applicationName string) (jose.JSONWebKeySet, error) {
jwks := jose.JSONWebKeySet{}
// Get certs - use application-specific cert if applicationName is provided
var certs []*Cert
var err error
if applicationName != "" {
// Try to get application-specific cert (owner is always "admin")
applicationId := util.GetId("admin", applicationName)
application, err := GetApplication(applicationId)
if err == nil && application != nil && application.Cert != "" {
certId := util.GetId(application.Owner, application.Cert)
cert, err := GetCert(certId)
if err == nil && cert != nil {
certs = []*Cert{cert}
}
}
}
// Fallback to global certs if no application-specific cert found
if len(certs) == 0 {
certs, err = GetCerts("")
if err != nil {
return jwks, err
}
}
// follows the protocol rfc 7517(draft)
// link here: https://self-issued.info/docs/draft-ietf-jose-json-web-key.html
// or https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key
for _, cert := range certs {
if cert.Type != "x509" {
continue
}
if cert.Certificate == "" {
return jwks, fmt.Errorf("the certificate field should not be empty for the cert: %v", cert)
}
certPemBlock := []byte(cert.Certificate)
certDerBlock, _ := pem.Decode(certPemBlock)
x509Cert, err := x509.ParseCertificate(certDerBlock.Bytes)
if err != nil {
return jwks, err
}
var jwk jose.JSONWebKey
jwk.Key = x509Cert.PublicKey
jwk.Certificates = []*x509.Certificate{x509Cert}
jwk.KeyID = cert.Name
jwk.Algorithm = cert.CryptoAlgorithm
jwk.Use = "sig"
jwks.Keys = append(jwks.Keys, jwk)
}
return jwks, nil
}
func GetWebFinger(resource string, rels []string, host string, applicationName string) (WebFinger, error) {
wf := WebFinger{}
resourceSplit := strings.Split(resource, ":")
if len(resourceSplit) != 2 {
return wf, fmt.Errorf("invalid resource")
}
resourceType := resourceSplit[0]
resourceValue := resourceSplit[1]
oidcDiscovery := GetOidcDiscovery(host, applicationName)
switch resourceType {
case "acct":
user, err := GetUserByEmailOnly(resourceValue)
if err != nil {
return wf, err
}
if user == nil {
return wf, fmt.Errorf("user not found")
}
wf.Subject = resource
for _, rel := range rels {
if rel == "http://openid.net/specs/connect/1.0/issuer" {
wf.Links = append(wf.Links, WebFingerLink{
Rel: "http://openid.net/specs/connect/1.0/issuer",
Href: oidcDiscovery.Issuer,
})
}
}
}
return wf, nil
}
func GetDeviceAuthResponse(deviceCode string, userCode string, host string) DeviceAuthResponse {
originFrontend, _ := getOriginFromHost(host)
return DeviceAuthResponse{
DeviceCode: deviceCode,
UserCode: userCode,
VerificationUri: fmt.Sprintf("%s/login/oauth/device/%s", originFrontend, userCode),
ExpiresIn: 120,
}
}