Skip to content

Commit 1c8d2dc

Browse files
authored
Merge pull request #6 from database-playground/pan93412/dbp-23-implement-oauth-token-introspection
DBP-23: Implement token introspection
2 parents 862f0ac + 65e2974 commit 1c8d2dc

File tree

6 files changed

+582
-5
lines changed

6 files changed

+582
-5
lines changed

httpapi/auth/README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,54 @@ Authorization: Bearer <access_token>
9292

9393
如果沒有 Auth Token 或者是 token 無效,則依然回傳 HTTP 200。請引導使用者重新登入。
9494

95+
## 取得 Token 資訊
96+
97+
您可以使用 `POST /api/auth/v2/introspect` 取得 token 的資訊。
98+
99+
需要帶入以 `application/x-www-form-urlencoded` 編碼的請求體:
100+
101+
- `token`:要 revoke 的 token
102+
- `token_type_hint`:必須是 `access_token`
103+
104+
如果有 token,回傳 HTTP 200,且 `active``true` 的回應:
105+
106+
```json
107+
{
108+
"active": true,
109+
"username": "[email protected]",
110+
"scope": "*", // scope, separated by space
111+
"sub": "1", // subject of token, a.k.a. user id
112+
"exp": 1757873526, // expired at
113+
"iat": 1757844711, // issued at
114+
"azp": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36" // the machine that is authorized to use this token
115+
}
116+
```
117+
118+
判斷管理員的依據,是判斷 `scope` 是否包含 `*`(所有權限)。
119+
120+
如果沒有此 token,回傳 HTTP 200,且 `active``false` 的回應:
121+
122+
```json
123+
{
124+
"active": false
125+
}
126+
```
127+
128+
如果發生系統錯誤,則回傳 HTTP 500 錯誤並帶上錯誤資訊:
129+
130+
```json
131+
{
132+
"error": "server_error",
133+
"error_description": "Failed to introspect the token. Please try again later."
134+
}
135+
```
136+
95137
## 參考來源
96138

97-
為了保證登入時的資訊安全,這裡參考了兩份 RFC 進行 API 的設計:
139+
為了保證登入時的資訊安全和規範性,這裡參考了這些資料進行 API 的設計:
98140

99-
- [RFC 6749 – The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749#autoid-35)
100-
- [RFC 7636 – Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
141+
- [RFC 6749 – The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
142+
- [RFC 7636 – Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)
101143
- [RFC 7009 – OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
144+
- [RFC 7662 – OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
145+
- [IANA – JSON Web Token Claims](https://www.iana.org/assignments/jwt/jwt.xhtml)

httpapi/auth/introspect.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package authservice
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"strconv"
7+
"strings"
8+
"time"
9+
10+
"github.com/database-playground/backend-v2/internal/auth"
11+
"github.com/database-playground/backend-v2/internal/useraccount"
12+
"github.com/gin-gonic/gin"
13+
)
14+
15+
// IntrospectionResponse represents the OAuth 2.0 token introspection response (RFC 7662)
16+
type IntrospectionResponse struct {
17+
Active bool `json:"active"`
18+
Username string `json:"username,omitempty"` // user email
19+
Scope string `json:"scope,omitempty"` // space-separated scopes
20+
Sub string `json:"sub,omitempty"` // subject (user ID)
21+
Exp int64 `json:"exp,omitempty"` // expiration time (Unix timestamp)
22+
Iat int64 `json:"iat,omitempty"` // issued at (Unix timestamp)
23+
Azp string `json:"azp,omitempty"` // authorized party (machine name)
24+
}
25+
26+
// IntrospectToken implements OAuth 2.0 Token Introspection (RFC 7662)
27+
// POST /api/auth/v2/introspect
28+
func (s *AuthService) IntrospectToken(c *gin.Context) {
29+
// Parse form data
30+
token := c.PostForm("token")
31+
tokenTypeHint := c.PostForm("token_type_hint")
32+
33+
// Validate required parameters
34+
if token == "" {
35+
c.JSON(http.StatusBadRequest, gin.H{
36+
"error": "invalid_request",
37+
"error_description": "Missing required parameter: token",
38+
})
39+
return
40+
}
41+
42+
// Validate token_type_hint if provided
43+
if tokenTypeHint != "" && tokenTypeHint != "access_token" {
44+
c.JSON(http.StatusBadRequest, gin.H{
45+
"error": "unsupported_token_type",
46+
"error_description": "Only access_token is supported for token_type_hint",
47+
})
48+
return
49+
}
50+
51+
// Try to peek the token (doesn't extend expiration)
52+
tokenInfo, err := s.storage.Peek(c.Request.Context(), token)
53+
if err != nil {
54+
if errors.Is(err, auth.ErrNotFound) {
55+
// Token not found or expired - return inactive token response
56+
c.JSON(http.StatusOK, IntrospectionResponse{
57+
Active: false,
58+
})
59+
return
60+
}
61+
62+
// Internal server error
63+
c.JSON(http.StatusInternalServerError, gin.H{
64+
"error": "server_error",
65+
"error_description": "Failed to introspect the token. Please try again later.",
66+
})
67+
return
68+
}
69+
70+
// Get user information
71+
useraccountCtx := useraccount.NewContext(s.entClient, s.storage)
72+
entUser, err := useraccountCtx.GetUser(c.Request.Context(), tokenInfo.UserID)
73+
if err != nil {
74+
if errors.Is(err, useraccount.ErrUserNotFound) {
75+
// User not found - token is technically invalid
76+
c.JSON(http.StatusOK, IntrospectionResponse{
77+
Active: false,
78+
})
79+
return
80+
}
81+
82+
// Internal server error
83+
c.JSON(http.StatusInternalServerError, gin.H{
84+
"error": "server_error",
85+
"error_description": err.Error(),
86+
})
87+
return
88+
}
89+
90+
// Calculate token expiration and issue time
91+
// Note: This is an approximation since we don't store these explicitly
92+
// We assume token is valid for DefaultTokenExpire seconds from now
93+
now := time.Now()
94+
exp := now.Add(time.Duration(auth.DefaultTokenExpire) * time.Second).Unix()
95+
iat := now.Unix() // Approximation - we don't have the actual issue time
96+
97+
// Build successful introspection response
98+
response := IntrospectionResponse{
99+
Active: true,
100+
Username: entUser.Email,
101+
Scope: strings.Join(tokenInfo.Scopes, " "),
102+
Sub: strconv.Itoa(tokenInfo.UserID),
103+
Exp: exp,
104+
Iat: iat,
105+
Azp: tokenInfo.Machine,
106+
}
107+
108+
c.JSON(http.StatusOK, response)
109+
}

0 commit comments

Comments
 (0)