|
|
|
|
|
package oauth
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/url"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/veypi/vbase/internal/model"
|
|
|
|
|
|
"github.com/veypi/vigo"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AuthorizeRequest 授权请求
|
|
|
|
|
|
type AuthorizeRequest struct {
|
|
|
|
|
|
ResponseType string `json:"response_type" src:"query" desc:"响应类型: code/token"`
|
|
|
|
|
|
ClientID string `json:"client_id" src:"query" desc:"客户端ID"`
|
|
|
|
|
|
RedirectURI string `json:"redirect_uri" src:"query" desc:"回调地址"`
|
|
|
|
|
|
Scope string `json:"scope" src:"query" desc:"请求的权限范围"`
|
|
|
|
|
|
State string `json:"state" src:"query" desc:"状态值(防CSRF)"`
|
|
|
|
|
|
CodeChallenge string `json:"code_challenge" src:"query" desc:"PKCE挑战码"`
|
|
|
|
|
|
CodeChallengeMethod string `json:"code_challenge_method" src:"query" desc:"PKCE方法: S256/plain"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AuthorizeResponse 授权响应 (用于重定向)
|
|
|
|
|
|
type AuthorizeResponse struct {
|
|
|
|
|
|
Code string `json:"code"`
|
|
|
|
|
|
State string `json:"state"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Authorize 授权端点 - 处理授权码请求
|
|
|
|
|
|
// GET /oauth/authorize
|
|
|
|
|
|
func Authorize(x *vigo.X, req *AuthorizeRequest) error {
|
|
|
|
|
|
// 验证必填参数
|
|
|
|
|
|
if req.ResponseType == "" {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "invalid_request", "response_type is required", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.ClientID == "" {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "invalid_request", "client_id is required", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找客户端
|
|
|
|
|
|
var client model.OAuthClient
|
|
|
|
|
|
if err := model.DB.Where("client_id = ? AND status = ?", req.ClientID, 1).First(&client).Error; err != nil {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "invalid_client", "client not found", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证response_type
|
|
|
|
|
|
if !strings.Contains(client.ResponseTypes, req.ResponseType) {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "unsupported_response_type", "", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证redirect_uri
|
|
|
|
|
|
if req.RedirectURI != "" {
|
|
|
|
|
|
allowedURIs := strings.Split(client.RedirectURIs, ",")
|
|
|
|
|
|
found := false
|
|
|
|
|
|
for _, uri := range allowedURIs {
|
|
|
|
|
|
if strings.TrimSpace(uri) == req.RedirectURI {
|
|
|
|
|
|
found = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !found {
|
|
|
|
|
|
return oauthError(x, "", "invalid_request", "redirect_uri mismatch", "")
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if client.RedirectURIs != "" {
|
|
|
|
|
|
// 使用第一个注册的回调地址
|
|
|
|
|
|
req.RedirectURI = strings.Split(client.RedirectURIs, ",")[0]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return oauthError(x, "", "invalid_request", "redirect_uri required", "")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证scope
|
|
|
|
|
|
requestedScopes := parseScopes(req.Scope)
|
|
|
|
|
|
allowedScopes := parseScopes(client.AllowedScopes)
|
|
|
|
|
|
for _, scope := range requestedScopes {
|
|
|
|
|
|
if !contains(allowedScopes, scope) {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "invalid_scope", "scope not allowed: "+scope, req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前用户
|
|
|
|
|
|
var userID string
|
|
|
|
|
|
if uid, ok := x.Get("current_user").(string); ok {
|
|
|
|
|
|
userID = uid
|
|
|
|
|
|
}
|
|
|
|
|
|
if userID == "" {
|
|
|
|
|
|
// 未登录,需要重定向到登录页面
|
|
|
|
|
|
loginURL := "/login?redirect=" + url.QueryEscape(x.Request.URL.String())
|
|
|
|
|
|
x.ResponseWriter().Header().Set("Location", loginURL)
|
|
|
|
|
|
x.ResponseWriter().WriteHeader(http.StatusFound)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取组织ID (从请求头或用户选择)
|
|
|
|
|
|
orgID := x.Request.Header.Get("X-Org-ID")
|
|
|
|
|
|
if orgID == "" && client.OrgID != "" {
|
|
|
|
|
|
orgID = client.OrgID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch req.ResponseType {
|
|
|
|
|
|
case model.ResponseTypeCode:
|
|
|
|
|
|
// 生成授权码
|
|
|
|
|
|
code, err := generateAuthorizationCode(&client, userID, orgID, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "server_error", "", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建重定向URL
|
|
|
|
|
|
redirectURL, _ := url.Parse(req.RedirectURI)
|
|
|
|
|
|
q := redirectURL.Query()
|
|
|
|
|
|
q.Set("code", code)
|
|
|
|
|
|
if req.State != "" {
|
|
|
|
|
|
q.Set("state", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
redirectURL.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
x.ResponseWriter().Header().Set("Location", redirectURL.String())
|
|
|
|
|
|
x.ResponseWriter().WriteHeader(http.StatusFound)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
|
|
case model.ResponseTypeToken:
|
|
|
|
|
|
// Implicit flow (简化模式) - 直接返回token
|
|
|
|
|
|
// 注意: 简化模式安全性较低,建议仅在必要时使用
|
|
|
|
|
|
return handleImplicitGrant(x, &client, userID, orgID, req)
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "unsupported_response_type", "", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TokenRequest 令牌请求
|
|
|
|
|
|
type TokenRequest struct {
|
|
|
|
|
|
GrantType string `json:"grant_type" src:"form" desc:"授权类型"`
|
|
|
|
|
|
Code string `json:"code" src:"form" desc:"授权码"`
|
|
|
|
|
|
RedirectURI string `json:"redirect_uri" src:"form" desc:"回调地址"`
|
|
|
|
|
|
ClientID string `json:"client_id" src:"form" desc:"客户端ID"`
|
|
|
|
|
|
ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"`
|
|
|
|
|
|
RefreshToken string `json:"refresh_token" src:"form" desc:"刷新令牌"`
|
|
|
|
|
|
Scope string `json:"scope" src:"form" desc:"权限范围"`
|
|
|
|
|
|
CodeVerifier string `json:"code_verifier" src:"form" desc:"PKCE验证器"`
|
|
|
|
|
|
Username string `json:"username" src:"form" desc:"用户名(密码模式)"`
|
|
|
|
|
|
Password string `json:"password" src:"form" desc:"密码(密码模式)"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TokenResponse 令牌响应
|
|
|
|
|
|
type TokenResponse struct {
|
|
|
|
|
|
AccessToken string `json:"access_token"`
|
|
|
|
|
|
TokenType string `json:"token_type"`
|
|
|
|
|
|
ExpiresIn int `json:"expires_in"`
|
|
|
|
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
|
|
|
|
Scope string `json:"scope,omitempty"`
|
|
|
|
|
|
IDToken string `json:"id_token,omitempty"` // OIDC
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Token 令牌端点 - 交换授权码获取访问令牌
|
|
|
|
|
|
// POST /oauth/token
|
|
|
|
|
|
func Token(x *vigo.X, req *TokenRequest) (*TokenResponse, error) {
|
|
|
|
|
|
// 验证客户端身份
|
|
|
|
|
|
client, err := authenticateClient(x, req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, vigo.ErrNotAuthorized.WithString("invalid_client")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证grant_type
|
|
|
|
|
|
if !strings.Contains(client.GrantTypes, req.GrantType) {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch req.GrantType {
|
|
|
|
|
|
case model.GrantTypeAuthorizationCode:
|
|
|
|
|
|
return handleAuthorizationCodeGrant(x, client, req)
|
|
|
|
|
|
case model.GrantTypeRefreshToken:
|
|
|
|
|
|
return handleRefreshTokenGrant(x, client, req)
|
|
|
|
|
|
case model.GrantTypeClientCredentials:
|
|
|
|
|
|
return handleClientCredentialsGrant(x, client, req)
|
|
|
|
|
|
case model.GrantTypePassword:
|
|
|
|
|
|
return handlePasswordGrant(x, client, req)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleAuthorizationCodeGrant 处理授权码模式
|
|
|
|
|
|
func handleAuthorizationCodeGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
|
|
|
|
|
|
if req.Code == "" {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("code is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找授权码
|
|
|
|
|
|
var auth model.OAuthAuthorization
|
|
|
|
|
|
if err := model.DB.Where("code = ? AND client_id = ? AND used = ?", req.Code, client.ClientID, false).First(&auth).Error; err != nil {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否过期
|
|
|
|
|
|
if time.Now().After(auth.ExpiresAt) {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code expired")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证redirect_uri
|
|
|
|
|
|
if req.RedirectURI != "" && req.RedirectURI != auth.RedirectURI {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: redirect_uri mismatch")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PKCE验证
|
|
|
|
|
|
if auth.CodeChallenge != "" {
|
|
|
|
|
|
if req.CodeVerifier == "" {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code_verifier required")
|
|
|
|
|
|
}
|
|
|
|
|
|
if !verifyPKCE(req.CodeVerifier, auth.CodeChallenge, auth.CodeChallengeMethod) {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code_verifier mismatch")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标记授权码为已使用
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
model.DB.Model(&auth).Updates(map[string]interface{}{
|
|
|
|
|
|
"used": true,
|
|
|
|
|
|
"used_at": now,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 生成访问令牌
|
|
|
|
|
|
tokenResp, err := generateTokenPair(client, auth.UserID, auth.OrgID, auth.Scope, client.TokenExpiry, client.RefreshExpiry)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, vigo.ErrInternalServer.WithError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tokenResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleRefreshTokenGrant 处理刷新令牌模式
|
|
|
|
|
|
func handleRefreshTokenGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
|
|
|
|
|
|
if req.RefreshToken == "" {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("refresh_token is required")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找刷新令牌
|
|
|
|
|
|
var token model.OAuthToken
|
|
|
|
|
|
if err := model.DB.Where("refresh_token = ? AND client_id = ? AND revoked = ?", req.RefreshToken, client.ClientID, false).First(&token).Error; err != nil {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否过期
|
|
|
|
|
|
if time.Now().After(token.ExpiresAt) {
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: token expired")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 撤销旧的刷新令牌
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
model.DB.Model(&token).Updates(map[string]interface{}{
|
|
|
|
|
|
"revoked": true,
|
|
|
|
|
|
"revoked_at": now,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 生成新的访问令牌
|
|
|
|
|
|
tokenResp, err := generateTokenPair(client, token.UserID, token.OrgID, token.Scope, client.TokenExpiry, client.RefreshExpiry)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, vigo.ErrInternalServer.WithError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tokenResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleClientCredentialsGrant 处理客户端凭证模式
|
|
|
|
|
|
func handleClientCredentialsGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
|
|
|
|
|
|
// 客户端凭证模式没有用户上下文,通常用于服务间调用
|
|
|
|
|
|
// 限制scope,只允许非用户相关的权限
|
|
|
|
|
|
|
|
|
|
|
|
requestedScopes := parseScopes(req.Scope)
|
|
|
|
|
|
allowedScopes := []string{}
|
|
|
|
|
|
for _, scope := range requestedScopes {
|
|
|
|
|
|
if scope == "service" || strings.HasPrefix(scope, "service:") {
|
|
|
|
|
|
allowedScopes = append(allowedScopes, scope)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(allowedScopes) == 0 {
|
|
|
|
|
|
allowedScopes = []string{"service"}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
scopeStr := strings.Join(allowedScopes, " ")
|
|
|
|
|
|
|
|
|
|
|
|
// 生成访问令牌
|
|
|
|
|
|
accessToken := generateRandomToken(32)
|
|
|
|
|
|
expiresAt := time.Now().Add(time.Duration(client.TokenExpiry) * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
token := &model.OAuthToken{
|
|
|
|
|
|
UserID: "", // 客户端凭证模式没有用户
|
|
|
|
|
|
ClientID: client.ClientID,
|
|
|
|
|
|
OrgID: client.OrgID,
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
TokenType: "Bearer",
|
|
|
|
|
|
Scope: scopeStr,
|
|
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := model.DB.Create(token).Error; err != nil {
|
|
|
|
|
|
return nil, vigo.ErrInternalServer.WithError(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &TokenResponse{
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
TokenType: "Bearer",
|
|
|
|
|
|
ExpiresIn: client.TokenExpiry,
|
|
|
|
|
|
Scope: scopeStr,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handlePasswordGrant 处理密码模式 (不推荐,但为兼容性保留)
|
|
|
|
|
|
func handlePasswordGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
|
|
|
|
|
|
// 密码模式需要验证用户凭据
|
|
|
|
|
|
// 这里简化处理,实际应该调用用户认证逻辑
|
|
|
|
|
|
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type: password grant is disabled")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleImplicitGrant 处理简化模式
|
|
|
|
|
|
func handleImplicitGrant(x *vigo.X, client *model.OAuthClient, userID, orgID string, req *AuthorizeRequest) error {
|
|
|
|
|
|
// 生成访问令牌
|
|
|
|
|
|
accessToken := generateRandomToken(32)
|
|
|
|
|
|
expiresAt := time.Now().Add(time.Duration(client.TokenExpiry) * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
token := &model.OAuthToken{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ClientID: client.ClientID,
|
|
|
|
|
|
OrgID: orgID,
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
TokenType: "Bearer",
|
|
|
|
|
|
Scope: req.Scope,
|
|
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := model.DB.Create(token).Error; err != nil {
|
|
|
|
|
|
return oauthError(x, req.RedirectURI, "server_error", "", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建fragment URL
|
|
|
|
|
|
fragment := url.Values{}
|
|
|
|
|
|
fragment.Set("access_token", accessToken)
|
|
|
|
|
|
fragment.Set("token_type", "Bearer")
|
|
|
|
|
|
fragment.Set("expires_in", string(rune(client.TokenExpiry)))
|
|
|
|
|
|
if req.Scope != "" {
|
|
|
|
|
|
fragment.Set("scope", req.Scope)
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.State != "" {
|
|
|
|
|
|
fragment.Set("state", req.State)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
redirectURL, _ := url.Parse(req.RedirectURI)
|
|
|
|
|
|
redirectURL.Fragment = fragment.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
x.ResponseWriter().Header().Set("Location", redirectURL.String())
|
|
|
|
|
|
x.ResponseWriter().WriteHeader(http.StatusFound)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RevokeRequest 撤销请求
|
|
|
|
|
|
type RevokeRequest struct {
|
|
|
|
|
|
Token string `json:"token" src:"form" desc:"要撤销的令牌"`
|
|
|
|
|
|
TokenTypeHint string `json:"token_type_hint" src:"form" desc:"令牌类型提示: access_token/refresh_token"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Revoke 撤销令牌端点
|
|
|
|
|
|
// POST /oauth/revoke
|
|
|
|
|
|
func Revoke(x *vigo.X, req *RevokeRequest) error {
|
|
|
|
|
|
if req.Token == "" {
|
|
|
|
|
|
return nil // 根据RFC 7009,无效的令牌也应返回200
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试查找access_token
|
|
|
|
|
|
var token model.OAuthToken
|
|
|
|
|
|
if err := model.DB.Where("access_token = ?", req.Token).First(&token).Error; err == nil {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
model.DB.Model(&token).Updates(map[string]interface{}{
|
|
|
|
|
|
"revoked": true,
|
|
|
|
|
|
"revoked_at": now,
|
|
|
|
|
|
})
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试查找refresh_token
|
|
|
|
|
|
if err := model.DB.Where("refresh_token = ?", req.Token).First(&token).Error; err == nil {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
model.DB.Model(&token).Updates(map[string]interface{}{
|
|
|
|
|
|
"revoked": true,
|
|
|
|
|
|
"revoked_at": now,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IntrospectRequest 令牌内省请求
|
|
|
|
|
|
type IntrospectRequest struct {
|
|
|
|
|
|
Token string `json:"token" src:"form" desc:"要内省的令牌"`
|
|
|
|
|
|
TokenTypeHint string `json:"token_type_hint" src:"form" desc:"令牌类型提示"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IntrospectResponse 令牌内省响应
|
|
|
|
|
|
type IntrospectResponse struct {
|
|
|
|
|
|
Active bool `json:"active"`
|
|
|
|
|
|
Scope string `json:"scope,omitempty"`
|
|
|
|
|
|
ClientID string `json:"client_id,omitempty"`
|
|
|
|
|
|
Username string `json:"username,omitempty"`
|
|
|
|
|
|
TokenType string `json:"token_type,omitempty"`
|
|
|
|
|
|
Exp int64 `json:"exp,omitempty"`
|
|
|
|
|
|
Iat int64 `json:"iat,omitempty"`
|
|
|
|
|
|
Sub string `json:"sub,omitempty"`
|
|
|
|
|
|
Aud string `json:"aud,omitempty"`
|
|
|
|
|
|
Iss string `json:"iss,omitempty"`
|
|
|
|
|
|
Jti string `json:"jti,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Introspect 令牌内省端点 (RFC 7662)
|
|
|
|
|
|
// POST /oauth/introspect
|
|
|
|
|
|
func Introspect(x *vigo.X, req *IntrospectRequest) (*IntrospectResponse, error) {
|
|
|
|
|
|
if req.Token == "" {
|
|
|
|
|
|
return &IntrospectResponse{Active: false}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var token model.OAuthToken
|
|
|
|
|
|
if err := model.DB.Where("access_token = ? AND revoked = ?", req.Token, false).First(&token).Error; err != nil {
|
|
|
|
|
|
return &IntrospectResponse{Active: false}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否过期
|
|
|
|
|
|
if time.Now().After(token.ExpiresAt) {
|
|
|
|
|
|
return &IntrospectResponse{Active: false}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
var user model.User
|
|
|
|
|
|
username := ""
|
|
|
|
|
|
if err := model.DB.First(&user, "id = ?", token.UserID).Error; err == nil {
|
|
|
|
|
|
username = user.Username
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &IntrospectResponse{
|
|
|
|
|
|
Active: true,
|
|
|
|
|
|
Scope: token.Scope,
|
|
|
|
|
|
ClientID: token.ClientID,
|
|
|
|
|
|
Username: username,
|
|
|
|
|
|
TokenType: token.TokenType,
|
|
|
|
|
|
Exp: token.ExpiresAt.Unix(),
|
|
|
|
|
|
Sub: token.UserID,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// helper functions
|
|
|
|
|
|
|
|
|
|
|
|
func generateAuthorizationCode(client *model.OAuthClient, userID, orgID string, req *AuthorizeRequest) (string, error) {
|
|
|
|
|
|
code := generateRandomToken(32)
|
|
|
|
|
|
expiresAt := time.Now().Add(10 * time.Minute) // 授权码10分钟有效
|
|
|
|
|
|
|
|
|
|
|
|
auth := &model.OAuthAuthorization{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ClientID: client.ClientID,
|
|
|
|
|
|
OrgID: orgID,
|
|
|
|
|
|
Code: code,
|
|
|
|
|
|
Scope: req.Scope,
|
|
|
|
|
|
State: req.State,
|
|
|
|
|
|
CodeChallenge: req.CodeChallenge,
|
|
|
|
|
|
CodeChallengeMethod: req.CodeChallengeMethod,
|
|
|
|
|
|
RedirectURI: req.RedirectURI,
|
|
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := model.DB.Create(auth).Error; err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return code, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func generateTokenPair(client *model.OAuthClient, userID, orgID, scope string, accessExpiry, refreshExpiry int) (*TokenResponse, error) {
|
|
|
|
|
|
accessToken := generateRandomToken(32)
|
|
|
|
|
|
refreshToken := generateRandomToken(32)
|
|
|
|
|
|
expiresAt := time.Now().Add(time.Duration(accessExpiry) * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
token := &model.OAuthToken{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ClientID: client.ClientID,
|
|
|
|
|
|
OrgID: orgID,
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
RefreshToken: refreshToken,
|
|
|
|
|
|
TokenType: "Bearer",
|
|
|
|
|
|
Scope: scope,
|
|
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := model.DB.Create(token).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &TokenResponse{
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
TokenType: "Bearer",
|
|
|
|
|
|
ExpiresIn: accessExpiry,
|
|
|
|
|
|
RefreshToken: refreshToken,
|
|
|
|
|
|
Scope: scope,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func generateRandomToken(length int) string {
|
|
|
|
|
|
b := make([]byte, length)
|
|
|
|
|
|
rand.Read(b)
|
|
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func authenticateClient(x *vigo.X, req *TokenRequest) (*model.OAuthClient, error) {
|
|
|
|
|
|
// 优先从Basic Auth获取
|
|
|
|
|
|
clientID, clientSecret, ok := x.Request.BasicAuth()
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
req.ClientID = clientID
|
|
|
|
|
|
req.ClientSecret = clientSecret
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if req.ClientID == "" || req.ClientSecret == "" {
|
|
|
|
|
|
return nil, vigo.ErrNotAuthorized
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var client model.OAuthClient
|
|
|
|
|
|
if err := model.DB.Where("client_id = ? AND client_secret = ? AND status = ?", req.ClientID, req.ClientSecret, 1).First(&client).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &client, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func verifyPKCE(verifier, challenge, method string) bool {
|
|
|
|
|
|
switch method {
|
|
|
|
|
|
case "S256":
|
|
|
|
|
|
hash := sha256.Sum256([]byte(verifier))
|
|
|
|
|
|
encoded := base64.RawURLEncoding.EncodeToString(hash[:])
|
|
|
|
|
|
return encoded == challenge
|
|
|
|
|
|
case "plain":
|
|
|
|
|
|
return verifier == challenge
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseScopes(scope string) []string {
|
|
|
|
|
|
if scope == "" {
|
|
|
|
|
|
return []string{}
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.Split(scope, " ")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func contains(arr []string, item string) bool {
|
|
|
|
|
|
for _, a := range arr {
|
|
|
|
|
|
if a == item {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func oauthError(x *vigo.X, redirectURI, errorCode, errorDescription, state string) error {
|
|
|
|
|
|
if redirectURI == "" {
|
|
|
|
|
|
return vigo.ErrArgInvalid.WithString(errorCode + ": " + errorDescription)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
u, err := url.Parse(redirectURI)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return vigo.ErrArgInvalid.WithString(errorCode + ": " + errorDescription)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
q := u.Query()
|
|
|
|
|
|
q.Set("error", errorCode)
|
|
|
|
|
|
if errorDescription != "" {
|
|
|
|
|
|
q.Set("error_description", errorDescription)
|
|
|
|
|
|
}
|
|
|
|
|
|
if state != "" {
|
|
|
|
|
|
q.Set("state", state)
|
|
|
|
|
|
}
|
|
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
|
|
|
|
x.ResponseWriter().Header().Set("Location", u.String())
|
|
|
|
|
|
x.ResponseWriter().WriteHeader(http.StatusFound)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|