Skip to content

Commit a37f353

Browse files
authored
feat: add generic OAuth authorization capability
Merged through pull request; the PR retains the complete reviewed commit history.
1 parent bfddea0 commit a37f353

14 files changed

Lines changed: 969 additions & 1 deletion

File tree

apps/workspace/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ MAX_BLOB_BYTES=536870912
2020
# Shared secret for POST /api/auth/discord/bot-login. Use at least 32 random characters.
2121
# DISCORD_BOT_API_KEY=replace-with-a-long-random-secret
2222

23+
# Register external OAuth-style clients. A JSON object with a `clients` array.
24+
# OAUTH_CLIENTS_JSON={"clients":[{"clientId":"...","clientSecret":"...","redirectUris":["https://client.example/callback"],"scopes":["identity"]}]}
25+
2326
# Optional override for the built-in 90-day browser runtime development license.
2427
# Required for deployments whose browser host is not localhost.
2528
VITE_UNIVER_LICENSE=

apps/workspace/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ shared key to a Discord client or browser. If the Bot initially supplies only
118118
OAuth login fills those placeholders from the verified Discord profile without
119119
replacing profile fields that the User has already customized.
120120

121+
Workspace exposes a generic OAuth-style authorization capability. A registered
122+
external client starts `GET /api/auth/authorize`; the authorize endpoint reuses
123+
`workspace_session`, redirecting through the existing login page only when the
124+
session is absent, then returns a one-time short-lived code to the registered
125+
redirect URI. `POST /api/auth/token` validates the client secret, the registered
126+
redirect URI, the PKCE verifier, expiry, and one-time use before returning the
127+
Workspace identity. Registration is deployment-supplied via `OAUTH_CLIENTS_JSON`.
128+
Existing Workspace login, OAuth callbacks, Cookie behavior, and product APIs
129+
remain unchanged. The capability is additive and does not add a proxy or
130+
deployment component.
131+
121132
The browser uses the same built-in runtime development license as Workspace
122133
CLI. Both copies are rotated every 90 days and are application credentials, not
123134
the repository software license. The built-in credential is for `localhost`;
@@ -155,6 +166,7 @@ docker run --name univer-workspace \
155166
-e DISCORD_CLIENT_ID \
156167
-e DISCORD_CLIENT_SECRET \
157168
-e DISCORD_CALLBACK_URL=https://workspace.univer.plus/api/auth/discord/callback \
169+
-e OAUTH_CLIENTS_JSON \
158170
-e SECURE_COOKIES=true \
159171
univer-workspace
160172
```

apps/workspace/contracts/http/openapi.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ security:
4242
paths:
4343
/api/session:
4444
$ref: ./paths/auth.yaml#/~1api~1session
45+
/api/auth/authorize:
46+
$ref: ./paths/auth.yaml#/~1api~1auth~1authorize
47+
/api/auth/token:
48+
$ref: ./paths/auth.yaml#/~1api~1auth~1token
4549
/api/auth/logout:
4650
$ref: ./paths/auth.yaml#/~1api~1auth~1logout
4751
/api/auth/password/register:

apps/workspace/contracts/http/paths/auth.yaml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,104 @@
1414
schema:
1515
$ref: ../schemas/identity.yaml#/SessionView
1616

17+
/api/auth/authorize:
18+
get:
19+
tags: [Authentication]
20+
operationId: oauthAuthorize
21+
summary: Start an OAuth-style authorization for a registered client.
22+
security:
23+
- {}
24+
- sessionCookie: []
25+
26+
parameters:
27+
- name: client_id
28+
in: query
29+
required: true
30+
schema:
31+
type: string
32+
- name: redirect_uri
33+
in: query
34+
required: true
35+
schema:
36+
type: string
37+
- name: state
38+
in: query
39+
required: true
40+
schema:
41+
type: string
42+
pattern: ^[A-Za-z0-9_-]{32,256}$
43+
- name: code_challenge
44+
in: query
45+
required: true
46+
schema:
47+
type: string
48+
- name: scope
49+
in: query
50+
required: false
51+
schema:
52+
type: string
53+
responses:
54+
"302":
55+
description: Redirect to Workspace login or back to the registered redirect_uri with a one-time code.
56+
headers:
57+
Location:
58+
required: true
59+
schema:
60+
type: string
61+
"400":
62+
$ref: ../schemas/common.yaml#/BadRequest
63+
64+
/api/auth/token:
65+
post:
66+
tags: [Authentication]
67+
operationId: oauthToken
68+
summary: Exchange a one-time authorization code for a registered client identity.
69+
security: []
70+
requestBody:
71+
required: true
72+
content:
73+
application/json:
74+
schema:
75+
type: object
76+
required: [code, client_id, client_secret, redirect_uri, code_verifier]
77+
properties:
78+
grant_type:
79+
type: string
80+
code:
81+
type: string
82+
client_id:
83+
type: string
84+
client_secret:
85+
type: string
86+
redirect_uri:
87+
type: string
88+
code_verifier:
89+
type: string
90+
responses:
91+
"200":
92+
description: The registered client identity.
93+
content:
94+
application/json:
95+
schema:
96+
type: object
97+
properties:
98+
access_token:
99+
type: string
100+
token_type:
101+
type: string
102+
expires_in:
103+
type: integer
104+
user:
105+
$ref: ../schemas/identity.yaml#/User
106+
"400":
107+
$ref: ../schemas/common.yaml#/BadRequest
108+
"401":
109+
description: Invalid client secret or PKCE verifier.
110+
content:
111+
application/json:
112+
schema:
113+
$ref: ../schemas/common.yaml#/ErrorResponse
114+
17115
/api/auth/logout:
18116
post:
19117
tags: [Session]

apps/workspace/docs/architecture.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ Express Request/Response 和 Univer SDK class 不进入业务 Module 的公开 I
164164

165165
Univer 集中在 `integrations/univer`,向业务 Module 提供产品语义的 Interface,不对 SDK
166166
方法做一一对应的空壳封装。外部 OAuth Provider 位于 Identity Module,并通过
167-
`GitHubOAuthProvider` / `DiscordOAuthProvider` Interface 在测试中替换。
167+
`GitHubOAuthProvider` / `DiscordOAuthProvider` Interface 在测试中替换。Identity Router
168+
为部署注册的 OAuth client 提供通用 authorize/token 交接:authorize 复用
169+
`workspace_session`,未登录时回到现有登录流程;token 只兑换一次性、短期、绑定 PKCE
170+
和已注册 redirect URI 的 code。Workspace Session 仍是唯一的身份权威来源,现有登录、
171+
Cookie、OAuth callback 和产品 API 保持原有行为;外部 client 只通过通用 OAuth 协议
172+
接入,代码不感知其业务身份。
168173

169174
跨产品数据库和 Collaboration Service 的写入由 `operations` Module 持久化和恢复,不用
170175
一次 SQLite transaction 假装覆盖两个系统。

apps/workspace/generated/http/openapi.bundled.yaml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,108 @@ paths:
5656
application/json:
5757
schema:
5858
$ref: '#/components/schemas/SessionView'
59+
/api/auth/authorize:
60+
get:
61+
tags:
62+
- Authentication
63+
operationId: oauthAuthorize
64+
summary: Start an OAuth-style authorization for a registered client.
65+
security:
66+
- {}
67+
- sessionCookie: []
68+
parameters:
69+
- name: client_id
70+
in: query
71+
required: true
72+
schema:
73+
type: string
74+
- name: redirect_uri
75+
in: query
76+
required: true
77+
schema:
78+
type: string
79+
- name: state
80+
in: query
81+
required: true
82+
schema:
83+
type: string
84+
pattern: ^[A-Za-z0-9_-]{32,256}$
85+
- name: code_challenge
86+
in: query
87+
required: true
88+
schema:
89+
type: string
90+
- name: scope
91+
in: query
92+
required: false
93+
schema:
94+
type: string
95+
responses:
96+
'302':
97+
description: Redirect to Workspace login or back to the registered redirect_uri with a one-time code.
98+
headers:
99+
Location:
100+
required: true
101+
schema:
102+
type: string
103+
'400':
104+
$ref: '#/components/responses/BadRequest'
105+
/api/auth/token:
106+
post:
107+
tags:
108+
- Authentication
109+
operationId: oauthToken
110+
summary: Exchange a one-time authorization code for a registered client identity.
111+
security: []
112+
requestBody:
113+
required: true
114+
content:
115+
application/json:
116+
schema:
117+
type: object
118+
required:
119+
- code
120+
- client_id
121+
- client_secret
122+
- redirect_uri
123+
- code_verifier
124+
properties:
125+
grant_type:
126+
type: string
127+
code:
128+
type: string
129+
client_id:
130+
type: string
131+
client_secret:
132+
type: string
133+
redirect_uri:
134+
type: string
135+
code_verifier:
136+
type: string
137+
responses:
138+
'200':
139+
description: The registered client identity.
140+
content:
141+
application/json:
142+
schema:
143+
type: object
144+
properties:
145+
access_token:
146+
type: string
147+
token_type:
148+
type: string
149+
expires_in:
150+
type: integer
151+
user:
152+
$ref: '#/components/schemas/User'
153+
'400':
154+
$ref: '#/components/responses/BadRequest'
155+
'401':
156+
description: Invalid client secret or PKCE verifier.
157+
content:
158+
application/json:
159+
schema:
160+
$ref: '#/components/schemas/ErrorResponse'
59161
/api/auth/logout:
60162
post:
61163
tags:

apps/workspace/generated/http/schema.d.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,40 @@ export interface paths {
2121
patch?: never;
2222
trace?: never;
2323
};
24+
"/api/auth/authorize": {
25+
parameters: {
26+
query?: never;
27+
header?: never;
28+
path?: never;
29+
cookie?: never;
30+
};
31+
/** Start an OAuth-style authorization for a registered client. */
32+
get: operations["oauthAuthorize"];
33+
put?: never;
34+
post?: never;
35+
delete?: never;
36+
options?: never;
37+
head?: never;
38+
patch?: never;
39+
trace?: never;
40+
};
41+
"/api/auth/token": {
42+
parameters: {
43+
query?: never;
44+
header?: never;
45+
path?: never;
46+
cookie?: never;
47+
};
48+
get?: never;
49+
put?: never;
50+
/** Exchange a one-time authorization code for a registered client identity. */
51+
post: operations["oauthToken"];
52+
delete?: never;
53+
options?: never;
54+
head?: never;
55+
patch?: never;
56+
trace?: never;
57+
};
2458
"/api/auth/logout": {
2559
parameters: {
2660
query?: never;
@@ -1867,6 +1901,78 @@ export interface operations {
18671901
};
18681902
};
18691903
};
1904+
oauthAuthorize: {
1905+
parameters: {
1906+
query: {
1907+
client_id: string;
1908+
redirect_uri: string;
1909+
state: string;
1910+
code_challenge: string;
1911+
scope?: string;
1912+
};
1913+
header?: never;
1914+
path?: never;
1915+
cookie?: never;
1916+
};
1917+
requestBody?: never;
1918+
responses: {
1919+
/** @description Redirect to Workspace login or back to the registered redirect_uri with a one-time code. */
1920+
302: {
1921+
headers: {
1922+
Location: string;
1923+
[name: string]: unknown;
1924+
};
1925+
content?: never;
1926+
};
1927+
400: components["responses"]["BadRequest"];
1928+
};
1929+
};
1930+
oauthToken: {
1931+
parameters: {
1932+
query?: never;
1933+
header?: never;
1934+
path?: never;
1935+
cookie?: never;
1936+
};
1937+
requestBody: {
1938+
content: {
1939+
"application/json": {
1940+
grant_type?: string;
1941+
code: string;
1942+
client_id: string;
1943+
client_secret: string;
1944+
redirect_uri: string;
1945+
code_verifier: string;
1946+
};
1947+
};
1948+
};
1949+
responses: {
1950+
/** @description The registered client identity. */
1951+
200: {
1952+
headers: {
1953+
[name: string]: unknown;
1954+
};
1955+
content: {
1956+
"application/json": {
1957+
access_token?: string;
1958+
token_type?: string;
1959+
expires_in?: number;
1960+
user?: components["schemas"]["User"];
1961+
};
1962+
};
1963+
};
1964+
400: components["responses"]["BadRequest"];
1965+
/** @description Invalid client secret or PKCE verifier. */
1966+
401: {
1967+
headers: {
1968+
[name: string]: unknown;
1969+
};
1970+
content: {
1971+
"application/json": components["schemas"]["ErrorResponse"];
1972+
};
1973+
};
1974+
};
1975+
};
18701976
logout: {
18711977
parameters: {
18721978
query?: never;

0 commit comments

Comments
 (0)