parent
26b6b1044b
commit
5a13b1037c
@ -0,0 +1,123 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const OIDCSigningPrivateKeySetting = "oidc_signing_private_key"
|
||||||
|
|
||||||
|
type OIDCIDTokenClaims struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
Nonce string `json:"nonce,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
EmailVerified bool `json:"email_verified,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWKSet struct {
|
||||||
|
Keys []JWK `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWK struct {
|
||||||
|
Kty string `json:"kty"`
|
||||||
|
Use string `json:"use"`
|
||||||
|
Alg string `json:"alg"`
|
||||||
|
Kid string `json:"kid"`
|
||||||
|
N string `json:"n"`
|
||||||
|
E string `json:"e"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SignOIDCIDToken(ctx context.Context, settingClient inventory.SettingClient, claims *OIDCIDTokenClaims) (string, error) {
|
||||||
|
key, kid, err := loadOrCreateOIDCSigningKey(ctx, settingClient)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
token.Header["kid"] = kid
|
||||||
|
return token.SignedString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OIDCJWKSet(ctx context.Context, settingClient inventory.SettingClient) (*JWKSet, error) {
|
||||||
|
key, kid, err := loadOrCreateOIDCSigningKey(ctx, settingClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &JWKSet{Keys: []JWK{buildJWK(&key.PublicKey, kid)}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrCreateOIDCSigningKey(ctx context.Context, settingClient inventory.SettingClient) (*rsa.PrivateKey, string, error) {
|
||||||
|
privateKeyRaw, err := settingClient.Get(ctx, OIDCSigningPrivateKeySetting)
|
||||||
|
if err != nil && !ent.IsNotFound(err) {
|
||||||
|
return nil, "", fmt.Errorf("failed to load OIDC signing key: %w", err)
|
||||||
|
}
|
||||||
|
if privateKeyRaw != "" {
|
||||||
|
key, err := parseRSAPrivateKey(privateKeyRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return key, oidcSigningKeyID(&key.PublicKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to generate OIDC signing key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyRaw = string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
}))
|
||||||
|
if err := settingClient.Set(ctx, map[string]string{OIDCSigningPrivateKeySetting: privateKeyRaw}); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to persist OIDC signing key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, oidcSigningKeyID(&key.PublicKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRSAPrivateKey(privateKeyRaw string) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(privateKeyRaw))
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("invalid OIDC signing key PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid OIDC signing key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func oidcSigningKeyID(key *rsa.PublicKey) string {
|
||||||
|
der, _ := x509.MarshalPKIXPublicKey(key)
|
||||||
|
sum := sha256.Sum256(der)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildJWK(key *rsa.PublicKey, kid string) JWK {
|
||||||
|
return JWK{
|
||||||
|
Kty: "RSA",
|
||||||
|
Use: "sig",
|
||||||
|
Alg: "RS256",
|
||||||
|
Kid: kid,
|
||||||
|
N: base64.RawURLEncoding.EncodeToString(key.N.Bytes()),
|
||||||
|
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.E)).Bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoveryService struct{}
|
||||||
|
|
||||||
|
type JWKService struct{}
|
||||||
|
|
||||||
|
func (s *DiscoveryService) Get(c *gin.Context) *DiscoveryResponse {
|
||||||
|
issuer := oidcIssuer(c)
|
||||||
|
return &DiscoveryResponse{
|
||||||
|
Issuer: issuer.String(),
|
||||||
|
AuthorizationEndpoint: oidcEndpoint(issuer, "/session/authorize"),
|
||||||
|
TokenEndpoint: oidcEndpoint(issuer, constants.APIPrefix+"/session/oauth/token"),
|
||||||
|
UserInfoEndpoint: oidcEndpoint(issuer, constants.APIPrefix+"/session/oauth/userinfo"),
|
||||||
|
JWKSURI: oidcEndpoint(issuer, constants.APIPrefix+"/session/oauth/jwks"),
|
||||||
|
ResponseTypesSupported: []string{
|
||||||
|
"code",
|
||||||
|
},
|
||||||
|
GrantTypesSupported: []string{
|
||||||
|
"authorization_code",
|
||||||
|
},
|
||||||
|
SubjectTypesSupported: []string{
|
||||||
|
"public",
|
||||||
|
},
|
||||||
|
IDTokenSigningAlgValuesSupported: []string{
|
||||||
|
"RS256",
|
||||||
|
},
|
||||||
|
TokenEndpointAuthMethods: []string{
|
||||||
|
"client_secret_post",
|
||||||
|
},
|
||||||
|
CodeChallengeMethodsSupported: []string{
|
||||||
|
"S256",
|
||||||
|
},
|
||||||
|
ScopesSupported: []string{
|
||||||
|
types.ScopeOpenID,
|
||||||
|
types.ScopeProfile,
|
||||||
|
types.ScopeEmail,
|
||||||
|
},
|
||||||
|
ClaimsSupported: []string{
|
||||||
|
"sub",
|
||||||
|
"name",
|
||||||
|
"preferred_username",
|
||||||
|
"picture",
|
||||||
|
"updated_at",
|
||||||
|
"email",
|
||||||
|
"email_verified",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JWKService) Get(c *gin.Context) (*auth.JWKSet, error) {
|
||||||
|
dep := dependency.FromContext(c)
|
||||||
|
return auth.OIDCJWKSet(c, dep.SettingClient())
|
||||||
|
}
|
||||||
|
|
||||||
|
func oidcIssuer(c *gin.Context) *url.URL {
|
||||||
|
dep := dependency.FromContext(c)
|
||||||
|
issuer := *dep.SettingProvider().SiteURL(setting.UseFirstSiteUrl(c))
|
||||||
|
issuer.RawQuery = ""
|
||||||
|
issuer.Fragment = ""
|
||||||
|
return &issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func oidcEndpoint(issuer *url.URL, endpoint string) string {
|
||||||
|
route, _ := url.Parse(endpoint)
|
||||||
|
return issuer.ResolveReference(route).String()
|
||||||
|
}
|
||||||
Loading…
Reference in new issue