mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,466 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// OIDCProvider manages OIDC operations
|
||||
type OIDCProvider struct {
|
||||
mu sync.RWMutex
|
||||
codes map[string]*AuthorizationCode
|
||||
sessions map[string]*OIDCSession
|
||||
config any // TODO: Use bridge.OIDCProviderConfig
|
||||
}
|
||||
|
||||
var (
|
||||
oidcProvider = &OIDCProvider{
|
||||
codes: make(map[string]*AuthorizationCode),
|
||||
sessions: make(map[string]*OIDCSession),
|
||||
}
|
||||
codeExpiration = 10 * time.Minute
|
||||
)
|
||||
|
||||
// SetOIDCConfig sets the OIDC provider configuration
|
||||
func SetOIDCConfig(config any) {
|
||||
oidcProvider.mu.Lock()
|
||||
defer oidcProvider.mu.Unlock()
|
||||
oidcProvider.config = config
|
||||
}
|
||||
|
||||
// GetOIDCDiscovery returns OIDC discovery configuration
|
||||
func GetOIDCDiscovery(c echo.Context) error {
|
||||
config := &OIDCConfig{
|
||||
Issuer: "https://localhost:8080",
|
||||
AuthorizationEndpoint: "https://localhost:8080/oidc/authorize",
|
||||
TokenEndpoint: "https://localhost:8080/oidc/token",
|
||||
UserInfoEndpoint: "https://localhost:8080/oidc/userinfo",
|
||||
JWKSEndpoint: "https://localhost:8080/oidc/jwks",
|
||||
RevocationEndpoint: "https://localhost:8080/oidc/revoke",
|
||||
IntrospectionEndpoint: "https://localhost:8080/oidc/introspect",
|
||||
ScopesSupported: []string{
|
||||
"openid", "profile", "email", "did", "vault", "offline_access",
|
||||
},
|
||||
ResponseTypesSupported: []string{
|
||||
"code", "id_token", "code id_token",
|
||||
},
|
||||
GrantTypesSupported: []string{
|
||||
"authorization_code", "refresh_token", "client_credentials",
|
||||
},
|
||||
SubjectTypesSupported: []string{
|
||||
"public", "pairwise",
|
||||
},
|
||||
IDTokenSigningAlgValuesSupported: []string{
|
||||
"ES256", "RS256",
|
||||
},
|
||||
TokenEndpointAuthMethodsSupported: []string{
|
||||
"client_secret_post", "client_secret_basic", "none",
|
||||
},
|
||||
ClaimsSupported: []string{
|
||||
"sub", "name", "preferred_username", "email", "email_verified",
|
||||
"did", "vault_id", "updated_at",
|
||||
},
|
||||
CodeChallengeMethodsSupported: []string{
|
||||
"S256", "plain",
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, config)
|
||||
}
|
||||
|
||||
// HandleOIDCAuthorization handles OIDC authorization requests
|
||||
func HandleOIDCAuthorization(c echo.Context) error {
|
||||
// Parse request parameters from query string for GET or form for POST
|
||||
req := OIDCAuthorizationRequest{
|
||||
ResponseType: c.QueryParam("response_type"),
|
||||
ClientID: c.QueryParam("client_id"),
|
||||
RedirectURI: c.QueryParam("redirect_uri"),
|
||||
Scope: c.QueryParam("scope"),
|
||||
State: c.QueryParam("state"),
|
||||
Nonce: c.QueryParam("nonce"),
|
||||
CodeChallenge: c.QueryParam("code_challenge"),
|
||||
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
|
||||
}
|
||||
|
||||
// If no query params, try to bind from body (for POST requests)
|
||||
if req.ResponseType == "" {
|
||||
_ = c.Bind(&req) // Ignore bind errors and continue
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if req.ResponseType == "" || req.ClientID == "" || req.RedirectURI == "" || req.Scope == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Missing required parameters",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate response type
|
||||
if req.ResponseType != "code" && req.ResponseType != "id_token" &&
|
||||
req.ResponseType != "code id_token" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "unsupported_response_type",
|
||||
"error_description": "Response type not supported",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate scope includes openid
|
||||
if !strings.Contains(req.Scope, "openid") {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_scope",
|
||||
"error_description": "Scope must include 'openid'",
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Validate client_id and redirect_uri against registered clients
|
||||
|
||||
// For now, assume user is authenticated (in production, redirect to login)
|
||||
userDID := c.Get("user_did")
|
||||
if userDID == nil {
|
||||
// Redirect to WebAuthn authentication
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{
|
||||
"error": "authentication_required",
|
||||
"error_description": "User authentication required",
|
||||
})
|
||||
}
|
||||
|
||||
// Generate authorization code
|
||||
code := generateAuthorizationCode()
|
||||
|
||||
// Store authorization code
|
||||
authCode := &AuthorizationCode{
|
||||
Code: code,
|
||||
ClientID: req.ClientID,
|
||||
RedirectURI: req.RedirectURI,
|
||||
UserDID: userDID.(string),
|
||||
Scope: req.Scope,
|
||||
Nonce: req.Nonce,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||
ExpiresAt: time.Now().Add(codeExpiration),
|
||||
Used: false,
|
||||
}
|
||||
|
||||
oidcProvider.mu.Lock()
|
||||
oidcProvider.codes[code] = authCode
|
||||
oidcProvider.mu.Unlock()
|
||||
|
||||
// Build redirect URL
|
||||
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", req.RedirectURI, code, req.State)
|
||||
|
||||
return c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
// HandleOIDCToken handles OIDC token requests
|
||||
func HandleOIDCToken(c echo.Context) error {
|
||||
var req OIDCTokenRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Invalid token request",
|
||||
})
|
||||
}
|
||||
|
||||
// Handle different grant types
|
||||
switch req.GrantType {
|
||||
case "authorization_code":
|
||||
return handleAuthorizationCodeGrant(c, &req)
|
||||
case "refresh_token":
|
||||
return handleRefreshTokenGrant(c, &req)
|
||||
case "client_credentials":
|
||||
return handleClientCredentialsGrant(c, &req)
|
||||
default:
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "Grant type not supported",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleAuthorizationCodeGrant processes authorization code grant
|
||||
func handleAuthorizationCodeGrant(c echo.Context, req *OIDCTokenRequest) error {
|
||||
if req.Code == "" || req.RedirectURI == "" || req.ClientID == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_request",
|
||||
"error_description": "Missing required parameters",
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieve and validate authorization code
|
||||
oidcProvider.mu.Lock()
|
||||
authCode, exists := oidcProvider.codes[req.Code]
|
||||
if exists {
|
||||
delete(oidcProvider.codes, req.Code) // Single use
|
||||
}
|
||||
oidcProvider.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid authorization code",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate code hasn't expired
|
||||
if time.Now().After(authCode.ExpiresAt) {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Authorization code expired",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate code hasn't been used
|
||||
if authCode.Used {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Authorization code already used",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate client and redirect URI
|
||||
if authCode.ClientID != req.ClientID || authCode.RedirectURI != req.RedirectURI {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid client or redirect URI",
|
||||
})
|
||||
}
|
||||
|
||||
// Validate PKCE if present
|
||||
if authCode.CodeChallenge != "" {
|
||||
if req.CodeVerifier == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Code verifier required",
|
||||
})
|
||||
}
|
||||
|
||||
if !verifyPKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "Invalid code verifier",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Mark code as used
|
||||
authCode.Used = true
|
||||
|
||||
// Generate tokens
|
||||
accessToken := generateAccessToken(authCode.UserDID)
|
||||
refreshToken := generateRefreshToken()
|
||||
idToken, err := generateIDToken(authCode.UserDID, authCode.ClientID, authCode.Nonce)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": "server_error",
|
||||
"error_description": "Failed to generate ID token",
|
||||
})
|
||||
}
|
||||
|
||||
// Create session
|
||||
session := &OIDCSession{
|
||||
SessionID: generateSessionID(),
|
||||
UserDID: authCode.UserDID,
|
||||
ClientID: authCode.ClientID,
|
||||
Scope: authCode.Scope,
|
||||
Nonce: authCode.Nonce,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
IDToken: idToken,
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
oidcProvider.mu.Lock()
|
||||
oidcProvider.sessions[accessToken] = session
|
||||
oidcProvider.mu.Unlock()
|
||||
|
||||
// Return tokens
|
||||
response := &OIDCTokenResponse{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
RefreshToken: refreshToken,
|
||||
IDToken: idToken,
|
||||
Scope: authCode.Scope,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleRefreshTokenGrant processes refresh token grant
|
||||
func handleRefreshTokenGrant(c echo.Context, req *OIDCTokenRequest) error {
|
||||
// TODO: Implement refresh token grant
|
||||
return c.JSON(http.StatusNotImplemented, map[string]string{
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "Refresh token grant not yet implemented",
|
||||
})
|
||||
}
|
||||
|
||||
// handleClientCredentialsGrant processes client credentials grant
|
||||
func handleClientCredentialsGrant(c echo.Context, req *OIDCTokenRequest) error {
|
||||
// TODO: Implement client credentials grant
|
||||
return c.JSON(http.StatusNotImplemented, map[string]string{
|
||||
"error": "unsupported_grant_type",
|
||||
"error_description": "Client credentials grant not yet implemented",
|
||||
})
|
||||
}
|
||||
|
||||
// HandleOIDCUserInfo handles OIDC userinfo requests
|
||||
func HandleOIDCUserInfo(c echo.Context) error {
|
||||
// Extract access token from Authorization header
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{
|
||||
"error": "invalid_token",
|
||||
"error_description": "Invalid access token",
|
||||
})
|
||||
}
|
||||
|
||||
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Validate access token
|
||||
oidcProvider.mu.RLock()
|
||||
session, exists := oidcProvider.sessions[accessToken]
|
||||
oidcProvider.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{
|
||||
"error": "invalid_token",
|
||||
"error_description": "Invalid or expired access token",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if time.Now().After(session.ExpiresAt) {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{
|
||||
"error": "invalid_token",
|
||||
"error_description": "Access token expired",
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Fetch actual user info from DID document or database
|
||||
userInfo := &OIDCUserInfo{
|
||||
Subject: session.UserDID,
|
||||
PreferredUsername: "user", // TODO: Get from DID document
|
||||
DID: session.UserDID,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add additional claims based on scope
|
||||
if strings.Contains(session.Scope, "profile") {
|
||||
userInfo.Name = "User Name" // TODO: Get from DID document
|
||||
}
|
||||
|
||||
if strings.Contains(session.Scope, "email") {
|
||||
userInfo.Email = "user@example.com" // TODO: Get from DID document
|
||||
userInfo.EmailVerified = true
|
||||
}
|
||||
|
||||
if strings.Contains(session.Scope, "vault") {
|
||||
userInfo.VaultID = "vault_" + session.UserDID // TODO: Get actual vault ID
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, userInfo)
|
||||
}
|
||||
|
||||
// HandleOIDCJWKS handles JWKS endpoint
|
||||
func HandleOIDCJWKS(c echo.Context) error {
|
||||
// TODO: Return actual public keys used for signing
|
||||
jwks := &JWKSet{
|
||||
Keys: []JWK{
|
||||
{
|
||||
KeyType: "EC",
|
||||
Use: "sig",
|
||||
KeyID: "1",
|
||||
Algorithm: "ES256",
|
||||
Curve: "P-256",
|
||||
// TODO: Add actual public key coordinates
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, jwks)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func generateAuthorizationCode() string {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func generateAccessToken(userDID string) string {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func generateRefreshToken() string {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func generateSessionID() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func generateIDToken(userDID, clientID, nonce string) (string, error) {
|
||||
claims := &DIDAuthClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userDID,
|
||||
Issuer: "https://localhost:8080",
|
||||
Audience: []string{clientID},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
DID: userDID,
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
claims.Extra = map[string]any{
|
||||
"nonce": nonce,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Sign with actual signing key
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte("temporary-secret-key"))
|
||||
}
|
||||
|
||||
func verifyPKCE(verifier, challenge, method string) bool {
|
||||
var computed string
|
||||
|
||||
switch method {
|
||||
case "S256":
|
||||
h := sha256.Sum256([]byte(verifier))
|
||||
computed = base64.RawURLEncoding.EncodeToString(h[:])
|
||||
case "plain":
|
||||
computed = verifier
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return computed == challenge
|
||||
}
|
||||
Reference in New Issue
Block a user