Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/workspace/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ MAX_BLOB_BYTES=536870912
# Shared secret for POST /api/auth/discord/bot-login. Use at least 32 random characters.
# DISCORD_BOT_API_KEY=replace-with-a-long-random-secret

# Register external OAuth-style clients. A JSON object with a `clients` array.
# OAUTH_CLIENTS_JSON={"clients":[{"clientId":"...","clientSecret":"...","redirectUris":["https://client.example/callback"],"scopes":["identity"]}]}

# Optional override for the built-in 90-day browser runtime development license.
# Required for deployments whose browser host is not localhost.
VITE_UNIVER_LICENSE=
12 changes: 12 additions & 0 deletions apps/workspace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ shared key to a Discord client or browser. If the Bot initially supplies only
OAuth login fills those placeholders from the verified Discord profile without
replacing profile fields that the User has already customized.

Workspace exposes a generic OAuth-style authorization capability. A registered
external client starts `GET /api/auth/authorize`; the authorize endpoint reuses
`workspace_session`, redirecting through the existing login page only when the
session is absent, then returns a one-time short-lived code to the registered
redirect URI. `POST /api/auth/token` validates the client secret, the registered
redirect URI, the PKCE verifier, expiry, and one-time use before returning the
Workspace identity. Registration is deployment-supplied via `OAUTH_CLIENTS_JSON`.
Existing Workspace login, OAuth callbacks, Cookie behavior, and product APIs
remain unchanged. The capability is additive and does not add a proxy or
deployment component.

The browser uses the same built-in runtime development license as Workspace
CLI. Both copies are rotated every 90 days and are application credentials, not
the repository software license. The built-in credential is for `localhost`;
Expand Down Expand Up @@ -155,6 +166,7 @@ docker run --name univer-workspace \
-e DISCORD_CLIENT_ID \
-e DISCORD_CLIENT_SECRET \
-e DISCORD_CALLBACK_URL=https://workspace.univer.plus/api/auth/discord/callback \
-e OAUTH_CLIENTS_JSON \
-e SECURE_COOKIES=true \
univer-workspace
```
Expand Down
4 changes: 4 additions & 0 deletions apps/workspace/contracts/http/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ security:
paths:
/api/session:
$ref: ./paths/auth.yaml#/~1api~1session
/api/auth/authorize:
$ref: ./paths/auth.yaml#/~1api~1auth~1authorize
/api/auth/token:
$ref: ./paths/auth.yaml#/~1api~1auth~1token
/api/auth/logout:
$ref: ./paths/auth.yaml#/~1api~1auth~1logout
/api/auth/password/register:
Expand Down
98 changes: 98 additions & 0 deletions apps/workspace/contracts/http/paths/auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,104 @@
schema:
$ref: ../schemas/identity.yaml#/SessionView

/api/auth/authorize:
get:
tags: [Authentication]
operationId: oauthAuthorize
summary: Start an OAuth-style authorization for a registered client.
security:
- {}
- sessionCookie: []

parameters:
- name: client_id
in: query
required: true
schema:
type: string
- name: redirect_uri
in: query
required: true
schema:
type: string
- name: state
in: query
required: true
schema:
type: string
pattern: ^[A-Za-z0-9_-]{32,256}$
- name: code_challenge
in: query
required: true
schema:
type: string
- name: scope
in: query
required: false
schema:
type: string
responses:
"302":
description: Redirect to Workspace login or back to the registered redirect_uri with a one-time code.
headers:
Location:
required: true
schema:
type: string
"400":
$ref: ../schemas/common.yaml#/BadRequest

/api/auth/token:
post:
tags: [Authentication]
operationId: oauthToken
summary: Exchange a one-time authorization code for a registered client identity.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code, client_id, client_secret, redirect_uri, code_verifier]
properties:
grant_type:
type: string
code:
type: string
client_id:
type: string
client_secret:
type: string
redirect_uri:
type: string
code_verifier:
type: string
responses:
"200":
description: The registered client identity.
content:
application/json:
schema:
type: object
properties:
access_token:
type: string
token_type:
type: string
expires_in:
type: integer
user:
$ref: ../schemas/identity.yaml#/User
"400":
$ref: ../schemas/common.yaml#/BadRequest
"401":
description: Invalid client secret or PKCE verifier.
content:
application/json:
schema:
$ref: ../schemas/common.yaml#/ErrorResponse

/api/auth/logout:
post:
tags: [Session]
Expand Down
7 changes: 6 additions & 1 deletion apps/workspace/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,12 @@ Express Request/Response 和 Univer SDK class 不进入业务 Module 的公开 I

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

跨产品数据库和 Collaboration Service 的写入由 `operations` Module 持久化和恢复,不用
一次 SQLite transaction 假装覆盖两个系统。
Expand Down
102 changes: 102 additions & 0 deletions apps/workspace/generated/http/openapi.bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,108 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/SessionView'
/api/auth/authorize:
get:
tags:
- Authentication
operationId: oauthAuthorize
summary: Start an OAuth-style authorization for a registered client.
security:
- {}
- sessionCookie: []
parameters:
- name: client_id
in: query
required: true
schema:
type: string
- name: redirect_uri
in: query
required: true
schema:
type: string
- name: state
in: query
required: true
schema:
type: string
pattern: ^[A-Za-z0-9_-]{32,256}$
- name: code_challenge
in: query
required: true
schema:
type: string
- name: scope
in: query
required: false
schema:
type: string
responses:
'302':
description: Redirect to Workspace login or back to the registered redirect_uri with a one-time code.
headers:
Location:
required: true
schema:
type: string
'400':
$ref: '#/components/responses/BadRequest'
/api/auth/token:
post:
tags:
- Authentication
operationId: oauthToken
summary: Exchange a one-time authorization code for a registered client identity.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- code
- client_id
- client_secret
- redirect_uri
- code_verifier
properties:
grant_type:
type: string
code:
type: string
client_id:
type: string
client_secret:
type: string
redirect_uri:
type: string
code_verifier:
type: string
responses:
'200':
description: The registered client identity.
content:
application/json:
schema:
type: object
properties:
access_token:
type: string
token_type:
type: string
expires_in:
type: integer
user:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
description: Invalid client secret or PKCE verifier.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/auth/logout:
post:
tags:
Expand Down
106 changes: 106 additions & 0 deletions apps/workspace/generated/http/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,40 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/auth/authorize": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Start an OAuth-style authorization for a registered client. */
get: operations["oauthAuthorize"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/auth/token": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Exchange a one-time authorization code for a registered client identity. */
post: operations["oauthToken"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/auth/logout": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1867,6 +1901,78 @@ export interface operations {
};
};
};
oauthAuthorize: {
parameters: {
query: {
client_id: string;
redirect_uri: string;
state: string;
code_challenge: string;
scope?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Redirect to Workspace login or back to the registered redirect_uri with a one-time code. */
302: {
headers: {
Location: string;
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["BadRequest"];
};
};
oauthToken: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
grant_type?: string;
code: string;
client_id: string;
client_secret: string;
redirect_uri: string;
code_verifier: string;
};
};
};
responses: {
/** @description The registered client identity. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
access_token?: string;
token_type?: string;
expires_in?: number;
user?: components["schemas"]["User"];
};
};
};
400: components["responses"]["BadRequest"];
/** @description Invalid client secret or PKCE verifier. */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
logout: {
parameters: {
query?: never;
Expand Down
Loading
Loading