mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -0,0 +1,923 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sonr-io/sonr/crypto/ucan"
|
||||
)
|
||||
|
||||
// OAuth2Provider extends OIDCProvider with full OAuth2 capabilities
|
||||
type OAuth2Provider struct {
|
||||
*OIDCProvider
|
||||
clientRegistry *ClientRegistry
|
||||
scopeMapper *ScopeMapper
|
||||
ucanDelegator *UCANDelegator
|
||||
authCodeStore *AuthCodeStore
|
||||
accessTokenStore *AccessTokenStore
|
||||
refreshTokenStore *RefreshTokenStore
|
||||
consentStore *ConsentStore
|
||||
config *OAuth2Config
|
||||
}
|
||||
|
||||
// AuthCodeStore manages authorization codes
|
||||
type AuthCodeStore struct {
|
||||
mu sync.RWMutex
|
||||
codes map[string]*OAuth2AuthorizationCode
|
||||
}
|
||||
|
||||
// AccessTokenStore manages access tokens
|
||||
type AccessTokenStore struct {
|
||||
mu sync.RWMutex
|
||||
tokens map[string]*OAuth2AccessToken
|
||||
}
|
||||
|
||||
// RefreshTokenStore manages refresh tokens
|
||||
type RefreshTokenStore struct {
|
||||
mu sync.RWMutex
|
||||
tokens map[string]*OAuth2RefreshToken
|
||||
}
|
||||
|
||||
// ConsentStore manages user consent records
|
||||
type ConsentStore struct {
|
||||
mu sync.RWMutex
|
||||
consents map[string]*UserConsent // key: userDID:clientID
|
||||
}
|
||||
|
||||
// UserConsent represents stored user consent
|
||||
type UserConsent struct {
|
||||
UserDID string `json:"user_did"`
|
||||
ClientID string `json:"client_id"`
|
||||
ApprovedScopes []string `json:"approved_scopes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
var oauth2Provider *OAuth2Provider
|
||||
|
||||
// InitializeOAuth2Provider initializes the OAuth2 provider
|
||||
func InitializeOAuth2Provider() {
|
||||
oauth2Provider = &OAuth2Provider{
|
||||
OIDCProvider: oidcProvider,
|
||||
clientRegistry: NewClientRegistry(),
|
||||
scopeMapper: NewScopeMapper(),
|
||||
ucanDelegator: NewUCANDelegator(nil),
|
||||
authCodeStore: &AuthCodeStore{codes: make(map[string]*OAuth2AuthorizationCode)},
|
||||
accessTokenStore: &AccessTokenStore{tokens: make(map[string]*OAuth2AccessToken)},
|
||||
refreshTokenStore: &RefreshTokenStore{tokens: make(map[string]*OAuth2RefreshToken)},
|
||||
consentStore: &ConsentStore{consents: make(map[string]*UserConsent)},
|
||||
config: getDefaultOAuth2Config(),
|
||||
}
|
||||
|
||||
// Start cleanup goroutine for expired tokens
|
||||
go oauth2Provider.cleanupExpiredTokens()
|
||||
}
|
||||
|
||||
// GetOAuth2Discovery returns OAuth2 discovery configuration
|
||||
func GetOAuth2Discovery(c echo.Context) error {
|
||||
if oauth2Provider == nil {
|
||||
InitializeOAuth2Provider()
|
||||
}
|
||||
return c.JSON(http.StatusOK, oauth2Provider.config)
|
||||
}
|
||||
|
||||
// HandleOAuth2Authorize handles OAuth2 authorization requests
|
||||
func HandleOAuth2Authorize(c echo.Context) error {
|
||||
if oauth2Provider == nil {
|
||||
InitializeOAuth2Provider()
|
||||
}
|
||||
|
||||
req := &OAuth2AuthorizationRequest{
|
||||
ResponseType: c.QueryParam("response_type"),
|
||||
ClientID: c.QueryParam("client_id"),
|
||||
RedirectURI: c.QueryParam("redirect_uri"),
|
||||
Scope: c.QueryParam("scope"),
|
||||
State: c.QueryParam("state"),
|
||||
CodeChallenge: c.QueryParam("code_challenge"),
|
||||
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
|
||||
Nonce: c.QueryParam("nonce"),
|
||||
Prompt: c.QueryParam("prompt"),
|
||||
LoginHint: c.QueryParam("login_hint"),
|
||||
}
|
||||
|
||||
// Validate client
|
||||
client, err := oauth2Provider.clientRegistry.GetClient(req.ClientID)
|
||||
if err != nil {
|
||||
return oauth2Error(c, "invalid_client", "Unknown client", req.State)
|
||||
}
|
||||
|
||||
// Validate redirect URI
|
||||
if !client.ValidateRedirectURI(req.RedirectURI) {
|
||||
return oauth2Error(c, "invalid_request", "Invalid redirect URI", req.State)
|
||||
}
|
||||
|
||||
// Validate response type
|
||||
if !isValidResponseType(req.ResponseType) {
|
||||
return redirectError(
|
||||
c,
|
||||
req.RedirectURI,
|
||||
"unsupported_response_type",
|
||||
"Response type not supported",
|
||||
req.State,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate scopes
|
||||
requestedScopes := parseScopes(req.Scope)
|
||||
if !client.ValidateScopes(requestedScopes) {
|
||||
return redirectError(
|
||||
c,
|
||||
req.RedirectURI,
|
||||
"invalid_scope",
|
||||
"Requested scope not allowed",
|
||||
req.State,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate PKCE for public clients
|
||||
if client.ClientType == "public" && client.RequirePKCE {
|
||||
if req.CodeChallenge == "" {
|
||||
return redirectError(
|
||||
c,
|
||||
req.RedirectURI,
|
||||
"invalid_request",
|
||||
"PKCE required for public clients",
|
||||
req.State,
|
||||
)
|
||||
}
|
||||
if req.CodeChallengeMethod != PKCEMethodS256 {
|
||||
return redirectError(
|
||||
c,
|
||||
req.RedirectURI,
|
||||
"invalid_request",
|
||||
"Only S256 PKCE method supported",
|
||||
req.State,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
userDID := c.Get("user_did")
|
||||
if userDID == nil {
|
||||
// Store authorization request and redirect to authentication
|
||||
sessionID := generateSessionID()
|
||||
// TODO: Store auth request in session store
|
||||
authURL := fmt.Sprintf(
|
||||
"/auth/login?session_id=%s&return_to=%s",
|
||||
sessionID,
|
||||
c.Request().URL.String(),
|
||||
)
|
||||
return c.Redirect(http.StatusFound, authURL)
|
||||
}
|
||||
|
||||
// Check consent
|
||||
if client.RequiresConsent &&
|
||||
!oauth2Provider.hasValidConsent(userDID.(string), req.ClientID, requestedScopes) {
|
||||
// Render consent page
|
||||
return renderOAuth2ConsentPage(c, req, client)
|
||||
}
|
||||
|
||||
// Generate authorization code
|
||||
code := generateSecureToken(32)
|
||||
authCode := &OAuth2AuthorizationCode{
|
||||
Code: code,
|
||||
ClientID: req.ClientID,
|
||||
UserDID: userDID.(string),
|
||||
RedirectURI: req.RedirectURI,
|
||||
Scopes: requestedScopes,
|
||||
State: req.State,
|
||||
Nonce: req.Nonce,
|
||||
CodeChallenge: req.CodeChallenge,
|
||||
CodeChallengeMethod: req.CodeChallengeMethod,
|
||||
ExpiresAt: time.Now().Add(10 * time.Minute),
|
||||
UCANContext: oauth2Provider.buildUCANContext(userDID.(string)),
|
||||
}
|
||||
|
||||
// Store authorization code
|
||||
oauth2Provider.authCodeStore.Store(authCode)
|
||||
|
||||
// Build redirect URL
|
||||
redirectURL := buildAuthorizationRedirect(req.RedirectURI, code, req.State)
|
||||
return c.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
// HandleOAuth2Token handles OAuth2 token requests
|
||||
func HandleOAuth2Token(c echo.Context) error {
|
||||
if oauth2Provider == nil {
|
||||
InitializeOAuth2Provider()
|
||||
}
|
||||
|
||||
var req OAuth2TokenRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return oauth2TokenError(c, "invalid_request", "Invalid token request")
|
||||
}
|
||||
|
||||
// Authenticate client
|
||||
client, err := oauth2Provider.authenticateClient(c, &req)
|
||||
if err != nil {
|
||||
return oauth2TokenError(c, "invalid_client", "Client authentication failed")
|
||||
}
|
||||
|
||||
// Handle grant type
|
||||
switch req.GrantType {
|
||||
case "authorization_code":
|
||||
return oauth2Provider.handleAuthorizationCodeGrant(c, client, &req)
|
||||
case "refresh_token":
|
||||
return oauth2Provider.handleRefreshTokenGrant(c, client, &req)
|
||||
case "client_credentials":
|
||||
return oauth2Provider.handleClientCredentialsGrant(c, client, &req)
|
||||
default:
|
||||
return oauth2TokenError(c, "unsupported_grant_type", "Grant type not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleOAuth2Introspection handles token introspection requests
|
||||
func HandleOAuth2Introspection(c echo.Context) error {
|
||||
if oauth2Provider == nil {
|
||||
InitializeOAuth2Provider()
|
||||
}
|
||||
|
||||
var req OAuth2IntrospectionRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, &OAuth2IntrospectionResponse{Active: false})
|
||||
}
|
||||
|
||||
// Authenticate client
|
||||
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
|
||||
ClientID: req.ClientID,
|
||||
ClientSecret: req.ClientSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, &OAuth2IntrospectionResponse{Active: false})
|
||||
}
|
||||
|
||||
// Introspect token
|
||||
response := oauth2Provider.introspectToken(req.Token, req.TokenTypeHint, client)
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// HandleOAuth2Revocation handles token revocation requests
|
||||
func HandleOAuth2Revocation(c echo.Context) error {
|
||||
if oauth2Provider == nil {
|
||||
InitializeOAuth2Provider()
|
||||
}
|
||||
|
||||
var req OAuth2RevocationRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.NoContent(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Authenticate client
|
||||
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
|
||||
ClientID: req.ClientID,
|
||||
ClientSecret: req.ClientSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return c.NoContent(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Revoke token
|
||||
oauth2Provider.revokeToken(req.Token, req.TokenTypeHint, client)
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
func (p *OAuth2Provider) handleAuthorizationCodeGrant(
|
||||
c echo.Context,
|
||||
client *OAuth2Client,
|
||||
req *OAuth2TokenRequest,
|
||||
) error {
|
||||
// Retrieve authorization code
|
||||
authCode := p.authCodeStore.Exchange(req.Code)
|
||||
if authCode == nil {
|
||||
return oauth2TokenError(c, "invalid_grant", "Invalid authorization code")
|
||||
}
|
||||
|
||||
// Validate code hasn't expired
|
||||
if time.Now().After(authCode.ExpiresAt) {
|
||||
return oauth2TokenError(c, "invalid_grant", "Authorization code expired")
|
||||
}
|
||||
|
||||
// Validate client
|
||||
if authCode.ClientID != client.ClientID {
|
||||
return oauth2TokenError(c, "invalid_grant", "Code was issued to different client")
|
||||
}
|
||||
|
||||
// Validate redirect URI
|
||||
if authCode.RedirectURI != req.RedirectURI {
|
||||
return oauth2TokenError(c, "invalid_grant", "Redirect URI mismatch")
|
||||
}
|
||||
|
||||
// Validate PKCE if present
|
||||
if authCode.CodeChallenge != "" {
|
||||
if !p.validatePKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
|
||||
return oauth2TokenError(c, "invalid_grant", "Invalid PKCE verifier")
|
||||
}
|
||||
}
|
||||
|
||||
// Create UCAN delegation
|
||||
ucanToken, err := p.ucanDelegator.CreateDelegation(
|
||||
authCode.UserDID,
|
||||
client.ClientID,
|
||||
authCode.Scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
return oauth2TokenError(c, "server_error", "Failed to create delegation")
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken := p.generateAccessToken(authCode, ucanToken)
|
||||
refreshToken := p.generateRefreshToken(authCode)
|
||||
|
||||
// Store tokens
|
||||
p.accessTokenStore.Store(accessToken)
|
||||
p.refreshTokenStore.Store(refreshToken)
|
||||
|
||||
// Generate ID token if openid scope present
|
||||
var idToken string
|
||||
if contains(authCode.Scopes, "openid") {
|
||||
idToken, _ = generateIDToken(authCode.UserDID, client.ClientID, authCode.Nonce)
|
||||
}
|
||||
|
||||
// Return token response
|
||||
response := &OAuth2TokenResponse{
|
||||
AccessToken: accessToken.Token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
RefreshToken: refreshToken.Token,
|
||||
Scope: strings.Join(authCode.Scopes, " "),
|
||||
IDToken: idToken,
|
||||
UCANToken: ucanToken.Raw,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) handleRefreshTokenGrant(
|
||||
c echo.Context,
|
||||
client *OAuth2Client,
|
||||
req *OAuth2TokenRequest,
|
||||
) error {
|
||||
// Retrieve refresh token
|
||||
oldRefreshToken := p.refreshTokenStore.Get(req.RefreshToken)
|
||||
if oldRefreshToken == nil {
|
||||
return oauth2TokenError(c, "invalid_grant", "Invalid refresh token")
|
||||
}
|
||||
|
||||
// Validate client
|
||||
if oldRefreshToken.ClientID != client.ClientID {
|
||||
return oauth2TokenError(c, "invalid_grant", "Token was issued to different client")
|
||||
}
|
||||
|
||||
// Validate expiration
|
||||
if time.Now().After(oldRefreshToken.ExpiresAt) {
|
||||
return oauth2TokenError(c, "invalid_grant", "Refresh token expired")
|
||||
}
|
||||
|
||||
// Rotate refresh token
|
||||
p.refreshTokenStore.Revoke(req.RefreshToken)
|
||||
|
||||
// Create new UCAN delegation
|
||||
ucanToken, err := p.ucanDelegator.CreateDelegation(
|
||||
oldRefreshToken.UserDID,
|
||||
client.ClientID,
|
||||
oldRefreshToken.Scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
return oauth2TokenError(c, "server_error", "Failed to create delegation")
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
newAccessToken := &OAuth2AccessToken{
|
||||
Token: generateSecureToken(32),
|
||||
UserDID: oldRefreshToken.UserDID,
|
||||
ClientID: client.ClientID,
|
||||
Scopes: oldRefreshToken.Scopes,
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
IssuedAt: time.Now(),
|
||||
UCANToken: ucanToken,
|
||||
}
|
||||
|
||||
newRefreshToken := &OAuth2RefreshToken{
|
||||
Token: generateSecureToken(32),
|
||||
AccessToken: newAccessToken.Token,
|
||||
ClientID: client.ClientID,
|
||||
UserDID: oldRefreshToken.UserDID,
|
||||
Scopes: oldRefreshToken.Scopes,
|
||||
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
|
||||
IssuedAt: time.Now(),
|
||||
RotationCount: oldRefreshToken.RotationCount + 1,
|
||||
}
|
||||
|
||||
// Store new tokens
|
||||
p.accessTokenStore.Store(newAccessToken)
|
||||
p.refreshTokenStore.Store(newRefreshToken)
|
||||
|
||||
// Return response
|
||||
response := &OAuth2TokenResponse{
|
||||
AccessToken: newAccessToken.Token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
RefreshToken: newRefreshToken.Token,
|
||||
Scope: strings.Join(newAccessToken.Scopes, " "),
|
||||
UCANToken: ucanToken.Raw,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) handleClientCredentialsGrant(
|
||||
c echo.Context,
|
||||
client *OAuth2Client,
|
||||
req *OAuth2TokenRequest,
|
||||
) error {
|
||||
// Client credentials grant is only for confidential clients
|
||||
if client.ClientType != "confidential" {
|
||||
return oauth2TokenError(
|
||||
c,
|
||||
"unauthorized_client",
|
||||
"Client type not authorized for this grant",
|
||||
)
|
||||
}
|
||||
|
||||
// Parse requested scopes
|
||||
scopes := parseScopes(req.Scope)
|
||||
if !client.ValidateScopes(scopes) {
|
||||
return oauth2TokenError(c, "invalid_scope", "Requested scope not allowed")
|
||||
}
|
||||
|
||||
// Create service-to-service UCAN token
|
||||
ucanToken, err := p.ucanDelegator.CreateServiceDelegation(
|
||||
client.ClientID,
|
||||
scopes,
|
||||
time.Now().Add(time.Hour),
|
||||
)
|
||||
if err != nil {
|
||||
return oauth2TokenError(c, "server_error", "Failed to create delegation")
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
accessToken := &OAuth2AccessToken{
|
||||
Token: generateSecureToken(32),
|
||||
UserDID: "", // No user for client credentials
|
||||
ClientID: client.ClientID,
|
||||
Scopes: scopes,
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
IssuedAt: time.Now(),
|
||||
UCANToken: ucanToken,
|
||||
TokenType: "client_credentials",
|
||||
}
|
||||
|
||||
// Store token
|
||||
p.accessTokenStore.Store(accessToken)
|
||||
|
||||
// Return response
|
||||
response := &OAuth2TokenResponse{
|
||||
AccessToken: accessToken.Token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
Scope: strings.Join(scopes, " "),
|
||||
UCANToken: ucanToken.Raw,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) authenticateClient(
|
||||
c echo.Context,
|
||||
req *OAuth2TokenRequest,
|
||||
) (*OAuth2Client, error) {
|
||||
// Try Basic Auth first
|
||||
if username, password, ok := c.Request().BasicAuth(); ok {
|
||||
client, err := p.clientRegistry.GetClient(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client.ClientType == "confidential" &&
|
||||
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(password)) == 1 {
|
||||
return client, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid client credentials")
|
||||
}
|
||||
|
||||
// Try client_secret_post
|
||||
if req.ClientID != "" && req.ClientSecret != "" {
|
||||
client, err := p.clientRegistry.GetClient(req.ClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client.ClientType == "confidential" &&
|
||||
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(req.ClientSecret)) == 1 {
|
||||
return client, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid client credentials")
|
||||
}
|
||||
|
||||
// Try client_assertion (JWT)
|
||||
if req.ClientAssertion != "" &&
|
||||
req.ClientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
|
||||
// TODO: Implement JWT client assertion validation
|
||||
return nil, fmt.Errorf("JWT client assertion not yet implemented")
|
||||
}
|
||||
|
||||
// Public client (no authentication)
|
||||
if req.ClientID != "" {
|
||||
client, err := p.clientRegistry.GetClient(req.ClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client.ClientType == "public" {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("client authentication required")
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) validatePKCE(verifier, challenge, method string) bool {
|
||||
if method == "" {
|
||||
method = PKCEMethodPlain
|
||||
}
|
||||
computed := computePKCEChallenge(verifier, method)
|
||||
return subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) == 1
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) hasValidConsent(userDID, clientID string, scopes []string) bool {
|
||||
p.consentStore.mu.RLock()
|
||||
defer p.consentStore.mu.RUnlock()
|
||||
|
||||
key := fmt.Sprintf("%s:%s", userDID, clientID)
|
||||
consent, exists := p.consentStore.consents[key]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if time.Now().After(consent.ExpiresAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check all requested scopes are approved
|
||||
for _, scope := range scopes {
|
||||
if !contains(consent.ApprovedScopes, scope) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) buildUCANContext(userDID string) *UCANAuthContext {
|
||||
// TODO: Fetch actual vault and DID document data
|
||||
return &UCANAuthContext{
|
||||
VaultAddress: fmt.Sprintf("vault_%s", userDID),
|
||||
EnclaveDataCID: fmt.Sprintf("cid_%s", userDID),
|
||||
Capabilities: []string{"read", "write", "sign"},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) generateAccessToken(
|
||||
authCode *OAuth2AuthorizationCode,
|
||||
ucanToken *ucan.Token,
|
||||
) *OAuth2AccessToken {
|
||||
return &OAuth2AccessToken{
|
||||
Token: generateSecureToken(32),
|
||||
UserDID: authCode.UserDID,
|
||||
ClientID: authCode.ClientID,
|
||||
Scopes: authCode.Scopes,
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
IssuedAt: time.Now(),
|
||||
UCANToken: ucanToken,
|
||||
SessionID: generateSessionID(),
|
||||
TokenType: "authorization_code",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) generateRefreshToken(
|
||||
authCode *OAuth2AuthorizationCode,
|
||||
) *OAuth2RefreshToken {
|
||||
return &OAuth2RefreshToken{
|
||||
Token: generateSecureToken(32),
|
||||
ClientID: authCode.ClientID,
|
||||
UserDID: authCode.UserDID,
|
||||
Scopes: authCode.Scopes,
|
||||
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
|
||||
IssuedAt: time.Now(),
|
||||
RotationCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) introspectToken(
|
||||
token, tokenTypeHint string,
|
||||
client *OAuth2Client,
|
||||
) *OAuth2IntrospectionResponse {
|
||||
// Try access token first
|
||||
if accessToken := p.accessTokenStore.Get(token); accessToken != nil {
|
||||
if accessToken.ClientID != client.ClientID {
|
||||
return &OAuth2IntrospectionResponse{Active: false}
|
||||
}
|
||||
return &OAuth2IntrospectionResponse{
|
||||
Active: time.Now().Before(accessToken.ExpiresAt),
|
||||
Scope: strings.Join(accessToken.Scopes, " "),
|
||||
ClientID: accessToken.ClientID,
|
||||
Username: accessToken.UserDID,
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: accessToken.ExpiresAt.Unix(),
|
||||
IssuedAt: accessToken.IssuedAt.Unix(),
|
||||
Subject: accessToken.UserDID,
|
||||
UCANToken: accessToken.UCANToken.Raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Try refresh token
|
||||
if refreshToken := p.refreshTokenStore.Get(token); refreshToken != nil {
|
||||
if refreshToken.ClientID != client.ClientID {
|
||||
return &OAuth2IntrospectionResponse{Active: false}
|
||||
}
|
||||
return &OAuth2IntrospectionResponse{
|
||||
Active: time.Now().Before(refreshToken.ExpiresAt),
|
||||
Scope: strings.Join(refreshToken.Scopes, " "),
|
||||
ClientID: refreshToken.ClientID,
|
||||
Username: refreshToken.UserDID,
|
||||
TokenType: "refresh_token",
|
||||
ExpiresAt: refreshToken.ExpiresAt.Unix(),
|
||||
IssuedAt: refreshToken.IssuedAt.Unix(),
|
||||
Subject: refreshToken.UserDID,
|
||||
}
|
||||
}
|
||||
|
||||
return &OAuth2IntrospectionResponse{Active: false}
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) revokeToken(token, tokenTypeHint string, client *OAuth2Client) {
|
||||
// Try to revoke as access token
|
||||
if p.accessTokenStore.Revoke(token) {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to revoke as refresh token
|
||||
p.refreshTokenStore.Revoke(token)
|
||||
}
|
||||
|
||||
func (p *OAuth2Provider) cleanupExpiredTokens() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
// Cleanup expired authorization codes
|
||||
p.authCodeStore.CleanupExpired()
|
||||
|
||||
// Cleanup expired access tokens
|
||||
p.accessTokenStore.CleanupExpired()
|
||||
|
||||
// Cleanup expired refresh tokens
|
||||
p.refreshTokenStore.CleanupExpired()
|
||||
}
|
||||
}
|
||||
|
||||
// Store methods for token stores
|
||||
|
||||
func (s *AuthCodeStore) Store(code *OAuth2AuthorizationCode) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.codes[code.Code] = code
|
||||
}
|
||||
|
||||
func (s *AuthCodeStore) Exchange(code string) *OAuth2AuthorizationCode {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
authCode, exists := s.codes[code]
|
||||
if !exists || authCode.Used {
|
||||
return nil
|
||||
}
|
||||
|
||||
authCode.Used = true
|
||||
return authCode
|
||||
}
|
||||
|
||||
func (s *AuthCodeStore) CleanupExpired() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for code, authCode := range s.codes {
|
||||
if now.After(authCode.ExpiresAt) {
|
||||
delete(s.codes, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AccessTokenStore) Store(token *OAuth2AccessToken) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.tokens[token.Token] = token
|
||||
}
|
||||
|
||||
func (s *AccessTokenStore) Get(token string) *OAuth2AccessToken {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tokens[token]
|
||||
}
|
||||
|
||||
func (s *AccessTokenStore) Revoke(token string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.tokens[token]; exists {
|
||||
delete(s.tokens, token)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *AccessTokenStore) CleanupExpired() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for token, accessToken := range s.tokens {
|
||||
if now.After(accessToken.ExpiresAt) {
|
||||
delete(s.tokens, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RefreshTokenStore) Store(token *OAuth2RefreshToken) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.tokens[token.Token] = token
|
||||
}
|
||||
|
||||
func (s *RefreshTokenStore) Get(token string) *OAuth2RefreshToken {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tokens[token]
|
||||
}
|
||||
|
||||
func (s *RefreshTokenStore) Revoke(token string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.tokens[token]; exists {
|
||||
delete(s.tokens, token)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *RefreshTokenStore) CleanupExpired() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for token, refreshToken := range s.tokens {
|
||||
if now.After(refreshToken.ExpiresAt) {
|
||||
delete(s.tokens, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func generateSecureToken(bytes int) string {
|
||||
b := make([]byte, bytes)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func parseScopes(scope string) []string {
|
||||
if scope == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(scope, " ")
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidResponseType(responseType string) bool {
|
||||
validTypes := []string{
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"id_token token",
|
||||
"code id_token token",
|
||||
}
|
||||
return contains(validTypes, responseType)
|
||||
}
|
||||
|
||||
func oauth2Error(c echo.Context, error, description, state string) error {
|
||||
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
|
||||
Error: error,
|
||||
ErrorDescription: description,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
func oauth2TokenError(c echo.Context, error, description string) error {
|
||||
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
|
||||
Error: error,
|
||||
ErrorDescription: description,
|
||||
})
|
||||
}
|
||||
|
||||
func redirectError(c echo.Context, redirectURI, error, description, state string) error {
|
||||
url := fmt.Sprintf("%s?error=%s&error_description=%s&state=%s",
|
||||
redirectURI, error, description, state)
|
||||
return c.Redirect(http.StatusFound, url)
|
||||
}
|
||||
|
||||
func buildAuthorizationRedirect(redirectURI, code, state string) string {
|
||||
if strings.Contains(redirectURI, "?") {
|
||||
return fmt.Sprintf("%s&code=%s&state=%s", redirectURI, code, state)
|
||||
}
|
||||
return fmt.Sprintf("%s?code=%s&state=%s", redirectURI, code, state)
|
||||
}
|
||||
|
||||
func renderOAuth2ConsentPage(
|
||||
c echo.Context,
|
||||
req *OAuth2AuthorizationRequest,
|
||||
client *OAuth2Client,
|
||||
) error {
|
||||
// TODO: Render actual consent page
|
||||
return c.JSON(http.StatusOK, map[string]any{
|
||||
"client": client,
|
||||
"scopes": parseScopes(req.Scope),
|
||||
"state": req.State,
|
||||
"client_id": req.ClientID,
|
||||
})
|
||||
}
|
||||
|
||||
func getDefaultOAuth2Config() *OAuth2Config {
|
||||
baseURL := "https://localhost:8080"
|
||||
return &OAuth2Config{
|
||||
Issuer: baseURL,
|
||||
AuthorizationEndpoint: baseURL + "/oauth2/authorize",
|
||||
TokenEndpoint: baseURL + "/oauth2/token",
|
||||
UserInfoEndpoint: baseURL + "/oauth2/userinfo",
|
||||
JWKSEndpoint: baseURL + "/oauth2/jwks",
|
||||
RegistrationEndpoint: baseURL + "/oauth2/register",
|
||||
IntrospectionEndpoint: baseURL + "/oauth2/introspect",
|
||||
RevocationEndpoint: baseURL + "/oauth2/revoke",
|
||||
ScopesSupported: []string{
|
||||
"openid", "profile", "email", "offline_access",
|
||||
"vault:read", "vault:write", "vault:sign", "vault:admin",
|
||||
"service:manage", "did:read", "did:write",
|
||||
},
|
||||
ResponseTypesSupported: []string{
|
||||
"code", "token", "id_token",
|
||||
"code id_token", "code token",
|
||||
"id_token token", "code id_token token",
|
||||
},
|
||||
ResponseModesSupported: []string{
|
||||
"query", "fragment", "form_post",
|
||||
},
|
||||
GrantTypesSupported: []string{
|
||||
"authorization_code", "implicit", "refresh_token",
|
||||
"client_credentials", "urn:ietf:params:oauth:grant-type:device_code",
|
||||
},
|
||||
SubjectTypesSupported: []string{
|
||||
"public", "pairwise",
|
||||
},
|
||||
IDTokenSigningAlgValuesSupported: []string{
|
||||
"ES256", "RS256", "HS256",
|
||||
},
|
||||
TokenEndpointAuthMethodsSupported: []string{
|
||||
"client_secret_basic", "client_secret_post",
|
||||
"client_secret_jwt", "private_key_jwt", "none",
|
||||
},
|
||||
ClaimsSupported: []string{
|
||||
"sub", "iss", "aud", "exp", "iat", "auth_time", "nonce",
|
||||
"name", "given_name", "family_name", "middle_name", "nickname",
|
||||
"preferred_username", "profile", "picture", "website", "email",
|
||||
"email_verified", "did", "vault_id", "ucan_capabilities",
|
||||
},
|
||||
CodeChallengeMethodsSupported: []string{
|
||||
PKCEMethodS256, PKCEMethodPlain,
|
||||
},
|
||||
ServiceDocumentation: baseURL + "/docs/oauth2",
|
||||
UILocalesSupported: []string{"en-US"},
|
||||
UCANSupported: true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user