Compare commits

...

5 Commits

Author SHA1 Message Date
Yang Luo
447aaba16e Delete object/token_resource_test.go 2026-02-15 18:02:06 +08:00
copilot-swe-agent[bot]
f56f38e32f Fix resource validation to strictly enforce RFC 8707 requirement
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-02-15 09:36:42 +00:00
copilot-swe-agent[bot]
f93e46c585 Add resource parameter validation tests and fix compilation errors
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-02-15 09:35:45 +00:00
copilot-swe-agent[bot]
af0a6ae4ef Add RFC 8707 resource parameter support to OAuth flow
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
2026-02-15 09:32:13 +00:00
copilot-swe-agent[bot]
b8821c761d Initial plan 2026-02-15 09:26:04 +00:00
8 changed files with 66 additions and 18 deletions

View File

@@ -323,7 +323,7 @@ func (c *ApiController) Signup() {
// If OAuth parameters are present, generate OAuth code and return it
if clientId != "" && responseType == ResponseTypeCode {
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
code, err := object.GetOAuthCode(userId, clientId, "", "password", responseType, redirectUri, scope, state, nonce, codeChallenge, "", c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error(), nil)
return

View File

@@ -161,12 +161,13 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
nonce := c.Ctx.Input.Query("nonce")
challengeMethod := c.Ctx.Input.Query("code_challenge_method")
codeChallenge := c.Ctx.Input.Query("code_challenge")
resource := c.Ctx.Input.Query("resource")
if challengeMethod != "S256" && challengeMethod != "null" && challengeMethod != "" {
c.ResponseError(c.T("auth:Challenge method should be S256"))
return
}
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, c.Ctx.Request.Host, c.GetAcceptLanguage())
code, err := object.GetOAuthCode(userId, clientId, form.Provider, form.SigninMethod, responseType, redirectUri, scope, state, nonce, codeChallenge, resource, c.Ctx.Request.Host, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error(), nil)
return

View File

@@ -176,6 +176,7 @@ func (c *ApiController) GetOAuthToken() {
subjectToken := c.Ctx.Input.Query("subject_token")
subjectTokenType := c.Ctx.Input.Query("subject_token_type")
audience := c.Ctx.Input.Query("audience")
resource := c.Ctx.Input.Query("resource")
if clientId == "" && clientSecret == "" {
clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
@@ -231,6 +232,9 @@ func (c *ApiController) GetOAuthToken() {
if audience == "" {
audience = tokenRequest.Audience
}
if resource == "" {
resource = tokenRequest.Resource
}
}
}
@@ -275,7 +279,7 @@ func (c *ApiController) GetOAuthToken() {
}
host := c.Ctx.Request.Host
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience)
token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, audience, resource)
if err != nil {
c.ResponseError(err.Error())
return

View File

@@ -30,4 +30,5 @@ type TokenRequest struct {
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
Audience string `json:"audience"`
Resource string `json:"resource"` // RFC 8707 Resource Indicator
}

View File

@@ -43,6 +43,7 @@ type Token struct {
CodeChallenge string `xorm:"varchar(100)" json:"codeChallenge"`
CodeIsUsed bool `json:"codeIsUsed"`
CodeExpireIn int64 `json:"codeExpireIn"`
Resource string `xorm:"varchar(255)" json:"resource"` // RFC 8707 Resource Indicator
}
func GetTokenCount(owner, organization, field, value string) (int64, error) {

View File

@@ -509,7 +509,7 @@ func refineUser(user *User) *User {
return user
}
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope string, host string) (string, string, string, error) {
func generateJwtToken(application *Application, user *User, provider string, signinMethod string, nonce string, scope string, resource string, host string) (string, string, string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(time.Duration(application.ExpireInHours * float64(time.Hour)))
refreshExpireTime := nowTime.Add(time.Duration(application.RefreshExpireInHours * float64(time.Hour)))
@@ -553,7 +553,10 @@ func generateJwtToken(application *Application, user *User, provider string, sig
},
}
if application.IsShared {
// RFC 8707: Use resource as audience when provided
if resource != "" {
claims.Audience = []string{resource}
} else if application.IsShared {
claims.Audience = []string{application.ClientId + "-org-" + user.Owner}
}

View File

@@ -18,6 +18,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"sync"
"time"
@@ -92,6 +93,26 @@ type DeviceAuthResponse struct {
Interval int `json:"interval"`
}
// validateResourceURI validates that the resource parameter is a valid absolute URI
// according to RFC 8707 Section 2
func validateResourceURI(resource string) error {
if resource == "" {
return nil // empty resource is allowed (backward compatibility)
}
parsedURL, err := url.Parse(resource)
if err != nil {
return fmt.Errorf("resource must be a valid URI")
}
// RFC 8707: The resource parameter must be an absolute URI
if !parsedURL.IsAbs() {
return fmt.Errorf("resource must be an absolute URI")
}
return nil
}
func ExpireTokenByAccessToken(accessToken string) (bool, *Application, *Token, error) {
token, err := GetTokenByAccessToken(accessToken)
if err != nil {
@@ -138,7 +159,7 @@ func CheckOAuthLogin(clientId string, responseType string, redirectUri string, s
return "", application, nil
}
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, host string, lang string) (*Code, error) {
func GetOAuthCode(userId string, clientId string, provider string, signinMethod string, responseType string, redirectUri string, scope string, state string, nonce string, challenge string, resource string, host string, lang string) (*Code, error) {
user, err := GetUser(userId)
if err != nil {
return nil, err
@@ -169,11 +190,19 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
}, nil
}
// Validate resource parameter (RFC 8707)
if err := validateResourceURI(resource); err != nil {
return &Code{
Message: err.Error(),
Code: "",
}, nil
}
err = ExtendUserWithRolesAndPermissions(user)
if err != nil {
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, provider, signinMethod, nonce, scope, resource, host)
if err != nil {
return nil, err
}
@@ -198,6 +227,7 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
CodeChallenge: challenge,
CodeIsUsed: false,
CodeExpireIn: time.Now().Add(time.Minute * 5).Unix(),
Resource: resource,
}
_, err = AddToken(token)
if err != nil {
@@ -210,7 +240,7 @@ func GetOAuthCode(userId string, clientId string, provider string, signinMethod
}, nil
}
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string) (interface{}, error) {
func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, audience string, resource string) (interface{}, error) {
application, err := GetApplicationByClientId(clientId)
if err != nil {
return nil, err
@@ -236,7 +266,7 @@ func GetOAuthToken(grantType string, clientId string, clientSecret string, code
var tokenError *TokenError
switch grantType {
case "authorization_code": // Authorization Code Grant
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier)
token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier, resource)
case "password": // Resource Owner Password Credentials Grant
token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
case "client_credentials": // Client Credentials Grant
@@ -391,7 +421,7 @@ func RefreshToken(grantType string, refreshToken string, scope string, clientId
return nil, err
}
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
newAccessToken, newRefreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return &TokenError{
Error: EndpointError,
@@ -545,7 +575,7 @@ func createGuestUserToken(application *Application, clientSecret string, verifie
}
// Generate JWT token
accessToken, refreshToken, tokenName, err := generateJwtToken(application, guestUser, "", "", "", "", "")
accessToken, refreshToken, tokenName, err := generateJwtToken(application, guestUser, "", "", "", "", "", "")
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -595,7 +625,7 @@ func generateGuestUsername() string {
// GetAuthorizationCodeToken
// Authorization code flow
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string) (*Token, *TokenError, error) {
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string, resource string) (*Token, *TokenError, error) {
if code == "" {
return nil, &TokenError{
Error: InvalidRequest,
@@ -663,6 +693,14 @@ func GetAuthorizationCodeToken(application *Application, clientSecret string, co
}, nil
}
// RFC 8707: Validate resource parameter matches the one in the authorization request
if resource != token.Resource {
return nil, &TokenError{
Error: InvalidGrant,
ErrorDescription: fmt.Sprintf("resource parameter does not match authorization request, expected: [%s], got: [%s]", token.Resource, resource),
}, nil
}
nowUnix := time.Now().Unix()
if nowUnix > token.CodeExpireIn {
// code must be used within 5 minutes
@@ -719,7 +757,7 @@ func GetPasswordToken(application *Application, username string, password string
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -765,7 +803,7 @@ func GetClientCredentialsToken(application *Application, clientSecret string, sc
Type: "application",
}
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, host)
accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -829,7 +867,7 @@ func GetTokenByUser(application *Application, user *User, scope string, nonce st
return nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, "", host)
if err != nil {
return nil, err
}
@@ -936,7 +974,7 @@ func GetWechatMiniProgramToken(application *Application, code string, host strin
return nil, nil, err
}
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,
@@ -1110,7 +1148,7 @@ func GetTokenExchangeToken(application *Application, clientSecret string, subjec
}
// Generate new JWT token
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, host)
accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
if err != nil {
return nil, &TokenError{
Error: EndpointError,

View File

@@ -89,7 +89,7 @@ func fastAutoSignin(ctx *context.Context) (string, error) {
return "", nil
}
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, ctx.Request.Host, getAcceptLanguage(ctx))
code, err := object.GetOAuthCode(userId, clientId, "", "autoSignin", responseType, redirectUri, scope, state, nonce, codeChallenge, "", ctx.Request.Host, getAcceptLanguage(ctx))
if err != nil {
return "", err
} else if code.Message != "" {