mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
370 lines
12 KiB
Protocol Buffer
370 lines
12 KiB
Protocol Buffer
syntax = "proto3";
|
|
|
|
package synctv.client;
|
|
|
|
import "proto/buf/validate/validate.proto";
|
|
import "proto/common.proto";
|
|
|
|
enum OAuth2ProviderType {
|
|
OAUTH2_PROVIDER_TYPE_UNSPECIFIED = 0;
|
|
OAUTH2_PROVIDER_TYPE_QQ = 1;
|
|
OAUTH2_PROVIDER_TYPE_GITHUB = 2;
|
|
OAUTH2_PROVIDER_TYPE_GOOGLE = 3;
|
|
OAUTH2_PROVIDER_TYPE_MICROSOFT = 4;
|
|
OAUTH2_PROVIDER_TYPE_DISCORD = 5;
|
|
OAUTH2_PROVIDER_TYPE_CASDOOR = 6;
|
|
OAUTH2_PROVIDER_TYPE_LOGTO = 7;
|
|
OAUTH2_PROVIDER_TYPE_OIDC = 8;
|
|
OAUTH2_PROVIDER_TYPE_FEISHU = 9;
|
|
OAUTH2_PROVIDER_TYPE_GITEE = 10;
|
|
OAUTH2_PROVIDER_TYPE_APPLE = 11;
|
|
}
|
|
|
|
enum OAuth2Operation {
|
|
OAUTH2_OPERATION_UNSPECIFIED = 0;
|
|
OAUTH2_OPERATION_LOGIN = 1;
|
|
OAUTH2_OPERATION_BIND = 2;
|
|
}
|
|
|
|
// Authorization modes supported by a provider instance.
|
|
enum OAuth2ProviderMode {
|
|
OAUTH2_PROVIDER_MODE_UNSPECIFIED = 0;
|
|
OAUTH2_PROVIDER_MODE_BROWSER = 1;
|
|
OAUTH2_PROVIDER_MODE_NATIVE = 2;
|
|
}
|
|
|
|
// ==================== OAuth2 Service ====================
|
|
// OAuth2/OIDC authentication service
|
|
//
|
|
// Frontend-driven flow:
|
|
// 1. Frontend calls GetAuthorizationUrl to get the OAuth2 provider's auth URL
|
|
// 2. Frontend redirects user to the auth URL
|
|
// 3. User authorizes on the OAuth2 provider (e.g., GitHub)
|
|
// 4. Provider redirects to frontend URL with code and state parameters
|
|
// 5. Frontend extracts code and state from URL
|
|
// 6. Frontend calls ExchangeAuthorizationCode with code and state
|
|
// 7. Backend validates state, exchanges code for user info, creates/logs in user
|
|
// 8. Backend returns JWT token to frontend
|
|
//
|
|
// Authentication:
|
|
// - GetAuthorizationUrl: None (public)
|
|
// - ExchangeAuthorizationCode: None for login flow; bind flow requires JWT matching OAuth2 state user
|
|
// - GetAuthorizationUrlForBind: JWT Authorization header (user_id)
|
|
// - ListAvailableProviders: None (public)
|
|
// - UnlinkProvider, GetLinkedProviders: JWT Authorization header (user_id)
|
|
//
|
|
// Routes: /api/oauth2/*
|
|
service OAuth2Service {
|
|
// Get authorization URL for OAuth2 login flow
|
|
// Returns the URL to redirect the user to for authorization
|
|
rpc GetAuthorizationUrl(GetAuthorizationUrlRequest) returns (GetAuthorizationUrlResponse);
|
|
|
|
// Get authorization URL for binding OAuth2 provider to existing user account
|
|
// Requires authentication
|
|
rpc GetAuthorizationUrlForBind(GetAuthorizationUrlForBindRequest) returns (GetAuthorizationUrlForBindResponse);
|
|
|
|
// Exchange authorization code for JWT token or complete a bind flow.
|
|
// Public for login flow. Bind flow requires authentication and the token's
|
|
// user ID must match the user stored in the OAuth2 state.
|
|
rpc ExchangeAuthorizationCode(ExchangeAuthorizationCodeRequest) returns (ExchangeAuthorizationCodeResponse);
|
|
|
|
// List all available OAuth2 provider instances
|
|
rpc ListAvailableProviders(ListAvailableProvidersRequest) returns (ListAvailableProvidersResponse);
|
|
|
|
// Unlink OAuth2 provider from user account (requires authentication)
|
|
rpc UnlinkProvider(UnlinkProviderRequest) returns (UnlinkProviderResponse);
|
|
|
|
// Get linked OAuth2 providers for authenticated user
|
|
rpc GetLinkedProviders(GetLinkedProvidersRequest) returns (GetLinkedProvidersResponse);
|
|
}
|
|
|
|
// ==================== Get Authorization URL ====================
|
|
|
|
message OAuth2ProviderInstancePathRequest {
|
|
string provider = 1 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 64
|
|
pattern: "^[A-Za-z0-9_-]+$"
|
|
}];
|
|
}
|
|
|
|
message OAuth2ProviderTypePathRequest {
|
|
string provider = 1 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 32
|
|
pattern: "^(qq|github|google|microsoft|discord|casdoor|logto|oidc|feishu|gitee|apple)$"
|
|
}];
|
|
}
|
|
|
|
message GetAuthorizationUrlRequest {
|
|
// OAuth2 provider instance name (e.g., "github", "google", "logto1")
|
|
string provider = 1 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 64
|
|
pattern: "^[A-Za-z0-9_-]+$"
|
|
}];
|
|
|
|
// Optional OAuth2 callback URL for this authorization flow.
|
|
// Clients use HTTPS App Links/Universal Links on mobile and loopback HTTP
|
|
// URLs on desktop. Custom URL schemes are not supported.
|
|
optional string redirect_url = 2 [(buf.validate.field).cel = {
|
|
id: "oauth2.get_authorization_url.redirect_url"
|
|
message: "redirect_url must be empty, an https URL, or a loopback http URL"
|
|
expression: "this == '' || (size(this) <= 2048 && (this.matches('^https://[^\\\\s]+$') || this.matches('^http://(127\\\\.0\\\\.0\\\\.1|localhost|\\\\[::1\\\\])(:[0-9]+)?/[^\\\\s]*$')))"
|
|
}];
|
|
// Ask the selected provider to use its native platform authorization flow.
|
|
optional bool native = 3;
|
|
}
|
|
|
|
message GetAuthorizationUrlResponse {
|
|
// Authorization URL to redirect user to
|
|
// Present for browser authorization and omitted for native authorization.
|
|
optional string authorization_url = 1;
|
|
|
|
// State token for CSRF protection
|
|
// Frontend should store this temporarily and validate it matches the state
|
|
// received in the callback URL parameters
|
|
string state = 2;
|
|
|
|
OAuth2Operation operation = 3 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// Provider-generated OIDC nonce when the authorization flow requires one.
|
|
optional string nonce = 4;
|
|
}
|
|
|
|
// ==================== Get Authorization URL for Bind ====================
|
|
|
|
message GetAuthorizationUrlForBindRequest {
|
|
// OAuth2 provider instance name (e.g., "github", "google", "logto1")
|
|
string provider = 1 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 64
|
|
pattern: "^[A-Za-z0-9_-]+$"
|
|
}];
|
|
|
|
// Optional OAuth2 callback URL for this bind flow.
|
|
// Clients use HTTPS App Links/Universal Links on mobile and loopback HTTP
|
|
// URLs on desktop. Custom URL schemes are not supported.
|
|
optional string redirect_url = 2 [(buf.validate.field).cel = {
|
|
id: "oauth2.get_authorization_url_for_bind.redirect_url"
|
|
message: "redirect_url must be empty, an https URL, or a loopback http URL"
|
|
expression: "this == '' || (size(this) <= 2048 && (this.matches('^https://[^\\\\s]+$') || this.matches('^http://(127\\\\.0\\\\.0\\\\.1|localhost|\\\\[::1\\\\])(:[0-9]+)?/[^\\\\s]*$')))"
|
|
}];
|
|
|
|
// Ask the selected provider to use its native platform authorization flow.
|
|
optional bool native = 4;
|
|
|
|
string verification_id = 3 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 128
|
|
}];
|
|
|
|
}
|
|
|
|
message GetAuthorizationUrlForBindResponse {
|
|
// Authorization URL to redirect user to
|
|
// Present for browser authorization and omitted for native authorization.
|
|
optional string authorization_url = 1;
|
|
|
|
// State token for CSRF protection
|
|
string state = 2;
|
|
|
|
OAuth2Operation operation = 3 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// Provider-generated OIDC nonce when the authorization flow requires one.
|
|
optional string nonce = 4;
|
|
}
|
|
|
|
// ==================== Exchange Authorization Code ====================
|
|
|
|
message ExchangeAuthorizationCodeRequest {
|
|
// Authorization code received from OAuth2 provider redirect
|
|
// Frontend extracts this from the callback URL query parameter
|
|
string code = 1 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 256
|
|
pattern: "^[A-Za-z0-9._+-]+$"
|
|
}];
|
|
|
|
// State token received from OAuth2 provider redirect
|
|
// The server uses this opaque token to recover the provider instance,
|
|
// redirect URL, PKCE verifier, OIDC nonce, and bind/login context.
|
|
string state = 2 [(buf.validate.field).string = {
|
|
len: 32
|
|
pattern: "^[A-Za-z0-9]+$"
|
|
}];
|
|
}
|
|
|
|
message ExchangeAuthorizationCodeResponse {
|
|
// JWT access token (if login flow)
|
|
// Empty if this is a bind flow
|
|
optional string access_token = 1;
|
|
|
|
// JWT refresh token (if login flow and refresh tokens are enabled)
|
|
optional string refresh_token = 2;
|
|
|
|
// Token expiration time in seconds
|
|
int64 expires_in = 3;
|
|
|
|
// User information (login flow only)
|
|
OAuth2UserInfo user_info = 4;
|
|
|
|
// Redirect URL from the original request (if provided)
|
|
// Frontend can use this to redirect user after successful authentication
|
|
optional string redirect_url = 5;
|
|
|
|
OAuth2Operation operation = 6 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// True when a first-time OAuth2 signup was accepted but requires admin review.
|
|
bool registration_review_required = 7;
|
|
|
|
// Public review request ID when registration_review_required is true.
|
|
optional string registration_review_id = 8;
|
|
}
|
|
|
|
// ==================== List Available Providers ====================
|
|
|
|
message ListAvailableProvidersRequest {
|
|
// Empty request
|
|
}
|
|
|
|
message ListAvailableProvidersResponse {
|
|
// List of available OAuth2 provider instances
|
|
repeated OAuth2ProviderInstance providers = 1;
|
|
}
|
|
|
|
message OAuth2ProviderInstance {
|
|
// Instance name (e.g., "github", "google", "logto1")
|
|
string name = 1;
|
|
|
|
OAuth2ProviderType type = 2 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// Whether this provider instance allows new local account creation.
|
|
bool signup_enabled = 3;
|
|
|
|
// Whether new account creation through this provider instance requires review.
|
|
bool signup_need_review = 4;
|
|
|
|
// Authorization modes implemented by this provider instance.
|
|
repeated OAuth2ProviderMode supported_modes = 5 [(buf.validate.field).repeated.items.enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
}
|
|
|
|
// ==================== Unlink Provider ====================
|
|
|
|
message UnlinkProviderRequest {
|
|
OAuth2ProviderType provider = 1 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// Optional: specific provider user ID to unlink
|
|
// If not provided, unlinks all bindings for this provider type
|
|
string provider_user_id = 2 [(buf.validate.field).cel = {
|
|
id: "oauth2.unlink_provider.provider_user_id"
|
|
message: "provider_user_id must be empty or at most 256 visible characters"
|
|
expression: "this == '' || (size(this) <= 256 && !this.matches('.*[\\\\x00-\\\\x1F\\\\x7F].*'))"
|
|
}];
|
|
|
|
// Required when provider_user_id is set: the configured provider instance namespace.
|
|
// Empty with empty provider_user_id unlinks every binding for provider type.
|
|
string provider_instance_name = 3 [
|
|
(buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
|
|
(buf.validate.field).string = {
|
|
pattern: "^[A-Za-z0-9_-]+$"
|
|
max_len: 64
|
|
}
|
|
];
|
|
|
|
string verification_id = 4 [(buf.validate.field).string = {
|
|
min_len: 1
|
|
max_len: 128
|
|
}];
|
|
|
|
option (buf.validate.message).cel = {
|
|
id: "oauth2.unlink_provider.instance_for_specific_identity"
|
|
message: "provider_instance_name is required when provider_user_id is set"
|
|
expression: "this.provider_user_id == '' || this.provider_instance_name != ''"
|
|
};
|
|
}
|
|
|
|
message UnlinkProviderResponse {
|
|
// Whether any provider was unlinked
|
|
bool success = 1;
|
|
|
|
// Number of provider bindings removed
|
|
int32 removed_count = 2;
|
|
}
|
|
|
|
// ==================== Get Linked Providers ====================
|
|
|
|
message GetLinkedProvidersRequest {
|
|
// Empty request (user_id comes from JWT)
|
|
}
|
|
|
|
message GetLinkedProvidersResponse {
|
|
// List of linked OAuth2 providers
|
|
repeated LinkedProvider providers = 1;
|
|
}
|
|
|
|
message LinkedProvider {
|
|
OAuth2ProviderType provider_type = 1 [(buf.validate.field).enum = {
|
|
defined_only: true
|
|
not_in: [0]
|
|
}];
|
|
|
|
// Username from the OAuth2 provider
|
|
string provider_username = 2;
|
|
|
|
// When the provider was linked (Unix timestamp, seconds)
|
|
// NOTE: Changed from string to int64 for consistency with all other timestamp fields.
|
|
int64 linked_at = 3;
|
|
|
|
// Provider instance name that owns this external identity namespace.
|
|
string provider_instance_name = 4;
|
|
|
|
// Issuer metadata when known for OIDC-like providers.
|
|
optional string provider_issuer = 5;
|
|
|
|
// External user ID in the provider instance namespace. Clients must pass
|
|
// this with provider_instance_name to unlink one specific OAuth2 identity.
|
|
string provider_user_id = 6;
|
|
}
|
|
|
|
// ==================== User Info ====================
|
|
|
|
message OAuth2UserInfo {
|
|
// User ID
|
|
string user_id = 1;
|
|
|
|
// Username
|
|
string username = 2;
|
|
|
|
// Avatar URL, when available.
|
|
optional string avatar = 3;
|
|
|
|
// User role
|
|
synctv.common.UserRole role = 4;
|
|
|
|
// Account status
|
|
synctv.common.UserStatus status = 5;
|
|
|
|
// When the account was created (Unix timestamp, seconds)
|
|
// NOTE: Changed from string to int64 for consistency with all other timestamp fields.
|
|
int64 created_at = 6;
|
|
}
|