mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-04-12 10:53:52 +00:00
fix(security): address all OAuth2 PR review issues
Agent-Logs-Url: https://github.com/bitechdev/ResolveSpec/sessions/e886b781-c910-425f-aa6f-06d13c46dcc7 Co-authored-by: warkanum <208308+warkanum@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
2a2e33da0c
commit
850ad2b2ab
@@ -217,10 +217,11 @@ auth := security.NewDatabaseAuthenticator(db).WithOAuth2(security.OAuth2Config{
|
|||||||
ProviderName: "google",
|
ProviderName: "google",
|
||||||
})
|
})
|
||||||
|
|
||||||
// nil = no password login; Google handles auth
|
// Pass `auth` so the OAuth server supports persistence, introspection, and revocation.
|
||||||
|
// Google handles the end-user authentication flow via redirect.
|
||||||
handler.EnableOAuthServer(security.OAuthServerConfig{
|
handler.EnableOAuthServer(security.OAuthServerConfig{
|
||||||
Issuer: "https://api.example.com",
|
Issuer: "https://api.example.com",
|
||||||
}, nil)
|
}, auth)
|
||||||
handler.RegisterOAuth2Provider(auth, "google")
|
handler.RegisterOAuth2Provider(auth, "google")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (h *Handler) RegisterOAuth2(auth *security.DatabaseAuthenticator, cfg OAuth
|
|||||||
//
|
//
|
||||||
// auth := security.NewGoogleAuthenticator(...)
|
// auth := security.NewGoogleAuthenticator(...)
|
||||||
// handler.RegisterOAuth2(auth, cfg)
|
// handler.RegisterOAuth2(auth, cfg)
|
||||||
// handler.EnableOAuthServer(resolvemcp.OAuthServerConfig{Issuer: "https://api.example.com"})
|
// handler.EnableOAuthServer(security.OAuthServerConfig{Issuer: "https://api.example.com"})
|
||||||
// security.RegisterSecurityHooks(handler, securityList)
|
// security.RegisterSecurityHooks(handler, securityList)
|
||||||
// http.ListenAndServe(":8080", handler.HTTPHandler(securityList))
|
// http.ListenAndServe(":8080", handler.HTTPHandler(securityList))
|
||||||
func (h *Handler) HTTPHandler(securityList *security.SecurityList) http.Handler {
|
func (h *Handler) HTTPHandler(securityList *security.SecurityList) http.Handler {
|
||||||
|
|||||||
@@ -938,14 +938,14 @@ cfg := security.OAuthServerConfig{
|
|||||||
|
|
||||||
| Field | Default | Notes |
|
| Field | Default | Notes |
|
||||||
|-------|---------|-------|
|
|-------|---------|-------|
|
||||||
| `Issuer` | — | Required |
|
| `Issuer` | — | Required; trailing slash is trimmed automatically |
|
||||||
| `ProviderCallbackPath` | `/oauth/provider/callback` | |
|
| `ProviderCallbackPath` | `/oauth/provider/callback` | |
|
||||||
| `LoginTitle` | `"Login"` | |
|
| `LoginTitle` | `"Sign in"` | |
|
||||||
| `PersistClients` | `false` | Set `true` for multi-instance |
|
| `PersistClients` | `false` | Set `true` for multi-instance |
|
||||||
| `PersistCodes` | `false` | Set `true` for multi-instance |
|
| `PersistCodes` | `false` | Set `true` for multi-instance; does not require `PersistClients` |
|
||||||
| `DefaultScopes` | `nil` | |
|
| `DefaultScopes` | `["openid","profile","email"]` | |
|
||||||
| `AccessTokenTTL` | `1h` | |
|
| `AccessTokenTTL` | `24h` | Also used as `expires_in` in token responses |
|
||||||
| `AuthCodeTTL` | `5m` | |
|
| `AuthCodeTTL` | `2m` | |
|
||||||
|
|
||||||
### Operating Modes
|
### Operating Modes
|
||||||
|
|
||||||
@@ -960,10 +960,11 @@ srv := security.NewOAuthServer(cfg, auth)
|
|||||||
|
|
||||||
**Mode 2 — External provider federation**
|
**Mode 2 — External provider federation**
|
||||||
|
|
||||||
Pass `nil` as auth and register external providers. The authorize page shows a provider selection UI.
|
Pass a `*DatabaseAuthenticator` for persistence (authorization codes, revoke, introspect) and register external providers. The authorize endpoint redirects to the specified provider (via the `provider` query param) or to the first registered provider by default.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
srv := security.NewOAuthServer(cfg, nil)
|
auth := security.NewDatabaseAuthenticator(db)
|
||||||
|
srv := security.NewOAuthServer(cfg, auth)
|
||||||
srv.RegisterExternalProvider(googleAuth, "google")
|
srv.RegisterExternalProvider(googleAuth, "google")
|
||||||
srv.RegisterExternalProvider(githubAuth, "github")
|
srv.RegisterExternalProvider(githubAuth, "github")
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1415,15 +1415,18 @@ CREATE TABLE IF NOT EXISTS oauth_clients (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- oauth_codes: short-lived authorization codes (for multi-instance deployments)
|
-- oauth_codes: short-lived authorization codes (for multi-instance deployments)
|
||||||
|
-- Note: client_id is stored without a foreign key so codes can be persisted even
|
||||||
|
-- when OAuth clients are managed in memory rather than persisted in oauth_clients.
|
||||||
CREATE TABLE IF NOT EXISTS oauth_codes (
|
CREATE TABLE IF NOT EXISTS oauth_codes (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
code VARCHAR(255) NOT NULL UNIQUE,
|
code VARCHAR(255) NOT NULL UNIQUE,
|
||||||
client_id VARCHAR(255) NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
|
client_id VARCHAR(255) NOT NULL,
|
||||||
redirect_uri TEXT NOT NULL,
|
redirect_uri TEXT NOT NULL,
|
||||||
client_state TEXT,
|
client_state TEXT,
|
||||||
code_challenge VARCHAR(255) NOT NULL,
|
code_challenge VARCHAR(255) NOT NULL,
|
||||||
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
||||||
session_token TEXT NOT NULL,
|
session_token TEXT NOT NULL,
|
||||||
|
refresh_token TEXT,
|
||||||
scopes TEXT[],
|
scopes TEXT[],
|
||||||
expires_at TIMESTAMP NOT NULL,
|
expires_at TIMESTAMP NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -1483,7 +1486,7 @@ CREATE OR REPLACE FUNCTION resolvespec_oauth_save_code(p_data jsonb)
|
|||||||
RETURNS TABLE(p_success bool, p_error text)
|
RETURNS TABLE(p_success bool, p_error text)
|
||||||
LANGUAGE plpgsql AS $$
|
LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO oauth_codes (code, client_id, redirect_uri, client_state, code_challenge, code_challenge_method, session_token, scopes, expires_at)
|
INSERT INTO oauth_codes (code, client_id, redirect_uri, client_state, code_challenge, code_challenge_method, session_token, refresh_token, scopes, expires_at)
|
||||||
VALUES (
|
VALUES (
|
||||||
p_data->>'code',
|
p_data->>'code',
|
||||||
p_data->>'client_id',
|
p_data->>'client_id',
|
||||||
@@ -1492,6 +1495,7 @@ BEGIN
|
|||||||
p_data->>'code_challenge',
|
p_data->>'code_challenge',
|
||||||
COALESCE(p_data->>'code_challenge_method', 'S256'),
|
COALESCE(p_data->>'code_challenge_method', 'S256'),
|
||||||
p_data->>'session_token',
|
p_data->>'session_token',
|
||||||
|
p_data->>'refresh_token',
|
||||||
ARRAY(SELECT jsonb_array_elements_text(p_data->'scopes')),
|
ARRAY(SELECT jsonb_array_elements_text(p_data->'scopes')),
|
||||||
(p_data->>'expires_at')::timestamp
|
(p_data->>'expires_at')::timestamp
|
||||||
);
|
);
|
||||||
@@ -1517,6 +1521,7 @@ BEGIN
|
|||||||
'code_challenge', code_challenge,
|
'code_challenge', code_challenge,
|
||||||
'code_challenge_method', code_challenge_method,
|
'code_challenge_method', code_challenge_method,
|
||||||
'session_token', session_token,
|
'session_token', session_token,
|
||||||
|
'refresh_token', refresh_token,
|
||||||
'scopes', to_jsonb(scopes)
|
'scopes', to_jsonb(scopes)
|
||||||
) INTO v_row;
|
) INTO v_row;
|
||||||
|
|
||||||
@@ -1540,7 +1545,7 @@ BEGIN
|
|||||||
'username', u.username,
|
'username', u.username,
|
||||||
'email', u.email,
|
'email', u.email,
|
||||||
'user_level', u.user_level,
|
'user_level', u.user_level,
|
||||||
'roles', to_jsonb(string_to_array(COALESCE(u.roles, ''), ',')),
|
'roles', COALESCE(to_jsonb(string_to_array(NULLIF(u.roles, ''), ',')), '[]'::jsonb),
|
||||||
'exp', EXTRACT(EPOCH FROM s.expires_at)::bigint,
|
'exp', EXTRACT(EPOCH FROM s.expires_at)::bigint,
|
||||||
'iat', EXTRACT(EPOCH FROM s.created_at)::bigint
|
'iat', EXTRACT(EPOCH FROM s.created_at)::bigint
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type OAuthServerConfig struct {
|
|||||||
ProviderCallbackPath string
|
ProviderCallbackPath string
|
||||||
|
|
||||||
// LoginTitle is shown on the built-in login form when the server acts as its own
|
// LoginTitle is shown on the built-in login form when the server acts as its own
|
||||||
// identity provider. Defaults to "MCP Login".
|
// identity provider. Defaults to "Sign in".
|
||||||
LoginTitle string
|
LoginTitle string
|
||||||
|
|
||||||
// PersistClients stores registered clients in the database when a DatabaseAuthenticator is provided.
|
// PersistClients stores registered clients in the database when a DatabaseAuthenticator is provided.
|
||||||
@@ -65,6 +65,7 @@ type pendingAuth struct {
|
|||||||
ProviderName string // empty = password login
|
ProviderName string // empty = password login
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
SessionToken string // set after authentication completes
|
SessionToken string // set after authentication completes
|
||||||
|
RefreshToken string // set after authentication completes when refresh tokens are issued
|
||||||
Scopes []string // requested scopes
|
Scopes []string // requested scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +101,8 @@ type OAuthServer struct {
|
|||||||
clients map[string]*oauthClient
|
clients map[string]*oauthClient
|
||||||
pending map[string]*pendingAuth // provider_state → pending (external flow)
|
pending map[string]*pendingAuth // provider_state → pending (external flow)
|
||||||
codes map[string]*pendingAuth // auth_code → pending (post-auth)
|
codes map[string]*pendingAuth // auth_code → pending (post-auth)
|
||||||
|
|
||||||
|
done chan struct{} // closed by Close() to stop background goroutines
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOAuthServer creates a new MCP OAuth2 authorization server.
|
// NewOAuthServer creates a new MCP OAuth2 authorization server.
|
||||||
@@ -107,6 +110,8 @@ type OAuthServer struct {
|
|||||||
// Pass a DatabaseAuthenticator to enable direct username/password login (the server
|
// Pass a DatabaseAuthenticator to enable direct username/password login (the server
|
||||||
// acts as its own identity provider). Pass nil to use only external providers.
|
// acts as its own identity provider). Pass nil to use only external providers.
|
||||||
// External providers are added separately via RegisterExternalProvider.
|
// External providers are added separately via RegisterExternalProvider.
|
||||||
|
//
|
||||||
|
// Call Close() to stop background goroutines when the server is no longer needed.
|
||||||
func NewOAuthServer(cfg OAuthServerConfig, auth *DatabaseAuthenticator) *OAuthServer {
|
func NewOAuthServer(cfg OAuthServerConfig, auth *DatabaseAuthenticator) *OAuthServer {
|
||||||
if cfg.ProviderCallbackPath == "" {
|
if cfg.ProviderCallbackPath == "" {
|
||||||
cfg.ProviderCallbackPath = "/oauth/provider/callback"
|
cfg.ProviderCallbackPath = "/oauth/provider/callback"
|
||||||
@@ -123,23 +128,40 @@ func NewOAuthServer(cfg OAuthServerConfig, auth *DatabaseAuthenticator) *OAuthSe
|
|||||||
if cfg.AuthCodeTTL == 0 {
|
if cfg.AuthCodeTTL == 0 {
|
||||||
cfg.AuthCodeTTL = 2 * time.Minute
|
cfg.AuthCodeTTL = 2 * time.Minute
|
||||||
}
|
}
|
||||||
|
// Normalize issuer: trim trailing slash to ensure consistent endpoint URL construction.
|
||||||
|
cfg.Issuer = strings.TrimRight(cfg.Issuer, "/")
|
||||||
s := &OAuthServer{
|
s := &OAuthServer{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
clients: make(map[string]*oauthClient),
|
clients: make(map[string]*oauthClient),
|
||||||
pending: make(map[string]*pendingAuth),
|
pending: make(map[string]*pendingAuth),
|
||||||
codes: make(map[string]*pendingAuth),
|
codes: make(map[string]*pendingAuth),
|
||||||
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
go s.cleanupExpired()
|
go s.cleanupExpired()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close stops the background goroutines started by NewOAuthServer.
|
||||||
|
// It is safe to call Close multiple times.
|
||||||
|
func (s *OAuthServer) Close() {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
// already closed
|
||||||
|
default:
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterExternalProvider adds an external OAuth2 provider (Google, GitHub, Microsoft, etc.)
|
// RegisterExternalProvider adds an external OAuth2 provider (Google, GitHub, Microsoft, etc.)
|
||||||
// that handles user authentication via redirect. The DatabaseAuthenticator must have been
|
// that handles user authentication via redirect. The DatabaseAuthenticator must have been
|
||||||
// configured with WithOAuth2(providerName, ...) before calling this.
|
// configured with WithOAuth2(providerName, ...) before calling this.
|
||||||
// Multiple providers can be registered; the first is used as the default.
|
// Multiple providers can be registered; the first is used as the default.
|
||||||
|
// All providers must be registered before the server starts serving requests.
|
||||||
func (s *OAuthServer) RegisterExternalProvider(auth *DatabaseAuthenticator, providerName string) {
|
func (s *OAuthServer) RegisterExternalProvider(auth *DatabaseAuthenticator, providerName string) {
|
||||||
|
s.mu.Lock()
|
||||||
s.providers = append(s.providers, externalProvider{auth: auth, providerName: providerName})
|
s.providers = append(s.providers, externalProvider{auth: auth, providerName: providerName})
|
||||||
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderCallbackPath returns the configured path for external provider callbacks.
|
// ProviderCallbackPath returns the configured path for external provider callbacks.
|
||||||
@@ -169,7 +191,11 @@ func (s *OAuthServer) HTTPHandler() http.Handler {
|
|||||||
func (s *OAuthServer) cleanupExpired() {
|
func (s *OAuthServer) cleanupExpired() {
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
for k, p := range s.pending {
|
for k, p := range s.pending {
|
||||||
@@ -185,6 +211,7 @@ func (s *OAuthServer) cleanupExpired() {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// RFC 8414 — Server metadata
|
// RFC 8414 — Server metadata
|
||||||
@@ -383,7 +410,7 @@ func (s *OAuthServer) authorizePost(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.issueCodeAndRedirect(w, r, loginResp.Token, clientID, redirectURI, clientState, codeChallenge, codeChallengeMethod, "", scopes)
|
s.issueCodeAndRedirect(w, r, loginResp.Token, loginResp.RefreshToken, clientID, redirectURI, clientState, codeChallenge, codeChallengeMethod, "", scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirectToExternalProvider stores the pending auth and redirects to the configured provider.
|
// redirectToExternalProvider stores the pending auth and redirects to the configured provider.
|
||||||
@@ -469,13 +496,13 @@ func (s *OAuthServer) providerCallbackHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.issueCodeAndRedirect(w, r, loginResp.Token,
|
s.issueCodeAndRedirect(w, r, loginResp.Token, loginResp.RefreshToken,
|
||||||
pending.ClientID, pending.RedirectURI, pending.ClientState,
|
pending.ClientID, pending.RedirectURI, pending.ClientState,
|
||||||
pending.CodeChallenge, pending.CodeChallengeMethod, pending.ProviderName, pending.Scopes)
|
pending.CodeChallenge, pending.CodeChallengeMethod, pending.ProviderName, pending.Scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// issueCodeAndRedirect generates a short-lived auth code and redirects to the MCP client.
|
// issueCodeAndRedirect generates a short-lived auth code and redirects to the MCP client.
|
||||||
func (s *OAuthServer) issueCodeAndRedirect(w http.ResponseWriter, r *http.Request, sessionToken, clientID, redirectURI, clientState, codeChallenge, codeChallengeMethod, providerName string, scopes []string) {
|
func (s *OAuthServer) issueCodeAndRedirect(w http.ResponseWriter, r *http.Request, sessionToken, refreshToken, clientID, redirectURI, clientState, codeChallenge, codeChallengeMethod, providerName string, scopes []string) {
|
||||||
authCode, err := randomOAuthToken()
|
authCode, err := randomOAuthToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
@@ -490,6 +517,7 @@ func (s *OAuthServer) issueCodeAndRedirect(w http.ResponseWriter, r *http.Reques
|
|||||||
CodeChallengeMethod: codeChallengeMethod,
|
CodeChallengeMethod: codeChallengeMethod,
|
||||||
ProviderName: providerName,
|
ProviderName: providerName,
|
||||||
SessionToken: sessionToken,
|
SessionToken: sessionToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
ExpiresAt: time.Now().Add(s.cfg.AuthCodeTTL),
|
ExpiresAt: time.Now().Add(s.cfg.AuthCodeTTL),
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
}
|
}
|
||||||
@@ -503,6 +531,7 @@ func (s *OAuthServer) issueCodeAndRedirect(w http.ResponseWriter, r *http.Reques
|
|||||||
CodeChallenge: codeChallenge,
|
CodeChallenge: codeChallenge,
|
||||||
CodeChallengeMethod: codeChallengeMethod,
|
CodeChallengeMethod: codeChallengeMethod,
|
||||||
SessionToken: sessionToken,
|
SessionToken: sessionToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
ExpiresAt: pending.ExpiresAt,
|
ExpiresAt: pending.ExpiresAt,
|
||||||
}
|
}
|
||||||
@@ -565,6 +594,7 @@ func (s *OAuthServer) handleAuthCodeGrant(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sessionToken string
|
var sessionToken string
|
||||||
|
var refreshToken string
|
||||||
var scopes []string
|
var scopes []string
|
||||||
|
|
||||||
if s.cfg.PersistCodes && s.auth != nil {
|
if s.cfg.PersistCodes && s.auth != nil {
|
||||||
@@ -586,6 +616,7 @@ func (s *OAuthServer) handleAuthCodeGrant(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
sessionToken = oauthCode.SessionToken
|
sessionToken = oauthCode.SessionToken
|
||||||
|
refreshToken = oauthCode.RefreshToken
|
||||||
scopes = oauthCode.Scopes
|
scopes = oauthCode.Scopes
|
||||||
} else {
|
} else {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -612,10 +643,11 @@ func (s *OAuthServer) handleAuthCodeGrant(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
sessionToken = pending.SessionToken
|
sessionToken = pending.SessionToken
|
||||||
|
refreshToken = pending.RefreshToken
|
||||||
scopes = pending.Scopes
|
scopes = pending.Scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOAuthToken(w, sessionToken, "", scopes)
|
s.writeOAuthToken(w, sessionToken, refreshToken, scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request) {
|
func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -634,7 +666,7 @@ func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request)
|
|||||||
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
s.writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -644,7 +676,7 @@ func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request)
|
|||||||
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
s.writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,6 +704,9 @@ func (s *OAuthServer) revokeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if s.auth != nil {
|
if s.auth != nil {
|
||||||
s.auth.OAuthRevokeToken(r.Context(), token) //nolint:errcheck
|
s.auth.OAuthRevokeToken(r.Context(), token) //nolint:errcheck
|
||||||
|
} else if len(s.providers) > 0 {
|
||||||
|
// In external-provider-only mode, attempt revocation via the first provider's auth.
|
||||||
|
s.providers[0].auth.OAuthRevokeToken(r.Context(), token) //nolint:errcheck
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -693,12 +728,22 @@ func (s *OAuthServer) introspectHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
token := r.FormValue("token")
|
token := r.FormValue("token")
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if token == "" || s.auth == nil {
|
if token == "" {
|
||||||
w.Write([]byte(`{"active":false}`)) //nolint:errcheck
|
w.Write([]byte(`{"active":false}`)) //nolint:errcheck
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := s.auth.OAuthIntrospectToken(r.Context(), token)
|
// Resolve the authenticator to use: prefer the primary auth, then the first provider's auth.
|
||||||
|
authToUse := s.auth
|
||||||
|
if authToUse == nil && len(s.providers) > 0 {
|
||||||
|
authToUse = s.providers[0].auth
|
||||||
|
}
|
||||||
|
if authToUse == nil {
|
||||||
|
w.Write([]byte(`{"active":false}`)) //nolint:errcheck
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := authToUse.OAuthIntrospectToken(r.Context(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte(`{"active":false}`)) //nolint:errcheck
|
w.Write([]byte(`{"active":false}`)) //nolint:errcheck
|
||||||
return
|
return
|
||||||
@@ -740,7 +785,7 @@ button{width:100%%;padding:.6rem;background:#0070f3;color:#fff;border:none;borde
|
|||||||
button:hover{background:#005fd4}.err{color:#d32f2f;margin-bottom:1rem;font-size:.875rem}</style>
|
button:hover{background:#005fd4}.err{color:#d32f2f;margin-bottom:1rem;font-size:.875rem}</style>
|
||||||
</head><body><div class="card">
|
</head><body><div class="card">
|
||||||
<h2>%s</h2>%s
|
<h2>%s</h2>%s
|
||||||
<form method="POST" action="/oauth/authorize">
|
<form method="POST" action="authorize">
|
||||||
<input type="hidden" name="client_id" value="%s">
|
<input type="hidden" name="client_id" value="%s">
|
||||||
<input type="hidden" name="redirect_uri" value="%s">
|
<input type="hidden" name="redirect_uri" value="%s">
|
||||||
<input type="hidden" name="client_state" value="%s">
|
<input type="hidden" name="client_state" value="%s">
|
||||||
@@ -815,18 +860,19 @@ func randomOAuthToken() (string, error) {
|
|||||||
|
|
||||||
func oauthSliceContains(slice []string, s string) bool {
|
func oauthSliceContains(slice []string, s string) bool {
|
||||||
for _, v := range slice {
|
for _, v := range slice {
|
||||||
if strings.EqualFold(v, s) {
|
if v == s {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOAuthToken(w http.ResponseWriter, accessToken, refreshToken string, scopes []string) {
|
func (s *OAuthServer) writeOAuthToken(w http.ResponseWriter, accessToken, refreshToken string, scopes []string) {
|
||||||
|
expiresIn := int64(s.cfg.AccessTokenTTL.Seconds())
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"access_token": accessToken,
|
"access_token": accessToken,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"expires_in": 86400,
|
"expires_in": expiresIn,
|
||||||
}
|
}
|
||||||
if refreshToken != "" {
|
if refreshToken != "" {
|
||||||
resp["refresh_token"] = refreshToken
|
resp["refresh_token"] = refreshToken
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type OAuthCode struct {
|
|||||||
CodeChallenge string `json:"code_challenge"`
|
CodeChallenge string `json:"code_challenge"`
|
||||||
CodeChallengeMethod string `json:"code_challenge_method"`
|
CodeChallengeMethod string `json:"code_challenge_method"`
|
||||||
SessionToken string `json:"session_token"`
|
SessionToken string `json:"session_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
Scopes []string `json:"scopes,omitempty"`
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ type OAuthTokenInfo struct {
|
|||||||
Sub string `json:"sub,omitempty"`
|
Sub string `json:"sub,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
UserLevel int `json:"user_level,omitempty"`
|
||||||
Roles []string `json:"roles,omitempty"`
|
Roles []string `json:"roles,omitempty"`
|
||||||
Exp int64 `json:"exp,omitempty"`
|
Exp int64 `json:"exp,omitempty"`
|
||||||
Iat int64 `json:"iat,omitempty"`
|
Iat int64 `json:"iat,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user