Skip to content

Commit 7932f6d

Browse files
authored
refactor: user auth improvements (#5360)
1 parent 2c2ef53 commit 7932f6d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+4655
-2673
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ web/dist
77
# Build artifacts
88
build/
99
bin/
10+
memos
11+
12+
# Plan/design documents
13+
docs/plans/
1014

1115
.DS_Store
1216

proto/api/v1/auth_service.proto

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,87 +11,104 @@ import "google/protobuf/timestamp.proto";
1111
option go_package = "gen/api/v1";
1212

1313
service AuthService {
14-
// GetCurrentSession returns the current active session information.
15-
// This method is idempotent and safe, suitable for checking current session state.
16-
rpc GetCurrentSession(GetCurrentSessionRequest) returns (GetCurrentSessionResponse) {
17-
option (google.api.http) = {get: "/api/v1/auth/sessions/current"};
14+
// GetCurrentUser returns the authenticated user's information.
15+
// Validates the access token and returns user details.
16+
// Similar to OIDC's /userinfo endpoint.
17+
rpc GetCurrentUser(GetCurrentUserRequest) returns (GetCurrentUserResponse) {
18+
option (google.api.http) = {get: "/api/v1/auth/me"};
1819
}
1920

20-
// CreateSession authenticates a user and creates a new session.
21-
// Returns the authenticated user information upon successful authentication.
22-
rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) {
21+
// SignIn authenticates a user with credentials and returns tokens.
22+
// On success, returns an access token and sets a refresh token cookie.
23+
// Supports password-based and SSO authentication methods.
24+
rpc SignIn(SignInRequest) returns (SignInResponse) {
2325
option (google.api.http) = {
24-
post: "/api/v1/auth/sessions"
26+
post: "/api/v1/auth/signin"
2527
body: "*"
2628
};
2729
}
2830

29-
// DeleteSession terminates the current user session.
30-
// This is an idempotent operation that invalidates the user's authentication.
31-
rpc DeleteSession(DeleteSessionRequest) returns (google.protobuf.Empty) {
32-
option (google.api.http) = {delete: "/api/v1/auth/sessions/current"};
31+
// SignOut terminates the user's authentication.
32+
// Revokes the refresh token and clears the authentication cookie.
33+
rpc SignOut(SignOutRequest) returns (google.protobuf.Empty) {
34+
option (google.api.http) = {post: "/api/v1/auth/signout"};
35+
}
36+
37+
// RefreshToken exchanges a valid refresh token for a new access token.
38+
// The refresh token is read from the HttpOnly cookie.
39+
// Returns a new short-lived access token.
40+
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse) {
41+
option (google.api.http) = {
42+
post: "/api/v1/auth/refresh"
43+
body: "*"
44+
};
3345
}
3446
}
3547

36-
message GetCurrentSessionRequest {}
48+
message GetCurrentUserRequest {}
3749

38-
message GetCurrentSessionResponse {
50+
message GetCurrentUserResponse {
51+
// The authenticated user's information.
3952
User user = 1;
40-
41-
// Last time the session was accessed.
42-
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
43-
google.protobuf.Timestamp last_accessed_at = 2;
4453
}
4554

46-
message CreateSessionRequest {
55+
message SignInRequest {
4756
// Nested message for password-based authentication credentials.
4857
message PasswordCredentials {
4958
// The username to sign in with.
50-
// Required field for password-based authentication.
5159
string username = 1 [(google.api.field_behavior) = REQUIRED];
5260

5361
// The password to sign in with.
54-
// Required field for password-based authentication.
5562
string password = 2 [(google.api.field_behavior) = REQUIRED];
5663
}
5764

5865
// Nested message for SSO authentication credentials.
5966
message SSOCredentials {
6067
// The ID of the SSO provider.
61-
// Required field to identify the SSO provider.
6268
int32 idp_id = 1 [(google.api.field_behavior) = REQUIRED];
6369

6470
// The authorization code from the SSO provider.
65-
// Required field for completing the SSO flow.
6671
string code = 2 [(google.api.field_behavior) = REQUIRED];
6772

6873
// The redirect URI used in the SSO flow.
69-
// Required field for security validation.
7074
string redirect_uri = 3 [(google.api.field_behavior) = REQUIRED];
7175

7276
// The PKCE code verifier for enhanced security (RFC 7636).
73-
// Optional field - if provided, enables PKCE flow protection against authorization code interception.
77+
// Optional - enables PKCE flow protection against authorization code interception.
7478
string code_verifier = 4 [(google.api.field_behavior) = OPTIONAL];
7579
}
7680

77-
// Provide one authentication method (username/password or SSO).
78-
// Required field to specify the authentication method.
81+
// Authentication credentials. Provide one method.
7982
oneof credentials {
80-
// Username and password authentication method.
83+
// Username and password authentication.
8184
PasswordCredentials password_credentials = 1;
8285

83-
// SSO provider authentication method.
86+
// SSO provider authentication.
8487
SSOCredentials sso_credentials = 2;
8588
}
8689
}
8790

88-
message CreateSessionResponse {
89-
// The authenticated user information.
91+
message SignInResponse {
92+
// The authenticated user's information.
9093
User user = 1;
9194

92-
// Last time the session was accessed.
93-
// Used for sliding expiration calculation (last_accessed_time + 2 weeks).
94-
google.protobuf.Timestamp last_accessed_at = 2;
95+
// The short-lived access token for API requests.
96+
// Store in memory only, not in localStorage.
97+
string access_token = 2;
98+
99+
// When the access token expires.
100+
// Client should call RefreshToken before this time.
101+
google.protobuf.Timestamp access_token_expires_at = 3;
95102
}
96103

97-
message DeleteSessionRequest {}
104+
message SignOutRequest {}
105+
106+
message RefreshTokenRequest {}
107+
108+
message RefreshTokenResponse {
109+
// The new short-lived access token.
110+
string access_token = 1;
111+
112+
// When the access token expires.
113+
google.protobuf.Timestamp expires_at = 2;
114+
}

proto/api/v1/user_service.proto

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -84,35 +84,39 @@ service UserService {
8484
option (google.api.method_signature) = "parent";
8585
}
8686

87-
// ListUserAccessTokens returns a list of access tokens for a user.
88-
rpc ListUserAccessTokens(ListUserAccessTokensRequest) returns (ListUserAccessTokensResponse) {
89-
option (google.api.http) = {get: "/api/v1/{parent=users/*}/accessTokens"};
87+
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
88+
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
89+
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
90+
option (google.api.http) = {get: "/api/v1/{parent=users/*}/personalAccessTokens"};
9091
option (google.api.method_signature) = "parent";
9192
}
9293

93-
// CreateUserAccessToken creates a new access token for a user.
94-
rpc CreateUserAccessToken(CreateUserAccessTokenRequest) returns (UserAccessToken) {
94+
// CreatePersonalAccessToken creates a new Personal Access Token for a user.
95+
// The token value is only returned once upon creation.
96+
rpc CreatePersonalAccessToken(CreatePersonalAccessTokenRequest) returns (CreatePersonalAccessTokenResponse) {
9597
option (google.api.http) = {
96-
post: "/api/v1/{parent=users/*}/accessTokens"
97-
body: "access_token"
98+
post: "/api/v1/{parent=users/*}/personalAccessTokens"
99+
body: "*"
98100
};
99-
option (google.api.method_signature) = "parent,access_token";
100101
}
101102

102-
// DeleteUserAccessToken deletes an access token.
103-
rpc DeleteUserAccessToken(DeleteUserAccessTokenRequest) returns (google.protobuf.Empty) {
104-
option (google.api.http) = {delete: "/api/v1/{name=users/*/accessTokens/*}"};
103+
// DeletePersonalAccessToken deletes a Personal Access Token.
104+
rpc DeletePersonalAccessToken(DeletePersonalAccessTokenRequest) returns (google.protobuf.Empty) {
105+
option (google.api.http) = {delete: "/api/v1/{name=users/*/personalAccessTokens/*}"};
105106
option (google.api.method_signature) = "name";
106107
}
107108

108-
// ListUserSessions returns a list of active sessions for a user.
109-
rpc ListUserSessions(ListUserSessionsRequest) returns (ListUserSessionsResponse) {
109+
// ListSessions returns a list of active login sessions for a user.
110+
// Each session represents a browser/device where the user is logged in.
111+
// Sessions are backed by refresh tokens with sliding expiration.
112+
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse) {
110113
option (google.api.http) = {get: "/api/v1/{parent=users/*}/sessions"};
111114
option (google.api.method_signature) = "parent";
112115
}
113116

114-
// RevokeUserSession revokes a specific session for a user.
115-
rpc RevokeUserSession(RevokeUserSessionRequest) returns (google.protobuf.Empty) {
117+
// RevokeSession revokes a specific login session.
118+
// This invalidates the refresh token, forcing re-authentication on that device.
119+
rpc RevokeSession(RevokeSessionRequest) returns (google.protobuf.Empty) {
116120
option (google.api.http) = {delete: "/api/v1/{name=users/*/sessions/*}"};
117121
option (google.api.method_signature) = "name";
118122
}
@@ -398,9 +402,9 @@ message UserSetting {
398402
KEY_UNSPECIFIED = 0;
399403
// GENERAL is the key for general user settings.
400404
GENERAL = 1;
401-
// SESSIONS is the key for user authentication sessions.
405+
// SESSIONS is the key for user login sessions (refresh tokens).
402406
SESSIONS = 2;
403-
// ACCESS_TOKENS is the key for access tokens.
407+
// ACCESS_TOKENS is the key for Personal Access Tokens (PATs).
404408
ACCESS_TOKENS = 3;
405409
// WEBHOOKS is the key for user webhooks.
406410
WEBHOOKS = 4;
@@ -420,14 +424,14 @@ message UserSetting {
420424

421425
// User authentication sessions configuration.
422426
message SessionsSetting {
423-
// List of active user sessions.
424-
repeated UserSession sessions = 1;
427+
// List of active login sessions.
428+
repeated Session sessions = 1;
425429
}
426430

427-
// User access tokens configuration.
431+
// Personal access tokens configuration.
428432
message AccessTokensSetting {
429-
// List of user access tokens.
430-
repeated UserAccessToken access_tokens = 1;
433+
// List of personal access tokens (PATs).
434+
repeated PersonalAccessToken personal_access_tokens = 1;
431435
}
432436

433437
// User webhooks configuration.
@@ -487,85 +491,97 @@ message ListUserSettingsResponse {
487491
int32 total_size = 3;
488492
}
489493

490-
// User access token message
491-
message UserAccessToken {
494+
// PersonalAccessToken represents a long-lived token for API/script access.
495+
// PATs are distinct from short-lived JWT access tokens used for session authentication.
496+
message PersonalAccessToken {
492497
option (google.api.resource) = {
493-
type: "memos.api.v1/UserAccessToken"
494-
pattern: "users/{user}/accessTokens/{access_token}"
495-
singular: "userAccessToken"
496-
plural: "userAccessTokens"
498+
type: "memos.api.v1/PersonalAccessToken"
499+
pattern: "users/{user}/personalAccessTokens/{personal_access_token}"
500+
singular: "personalAccessToken"
501+
plural: "personalAccessTokens"
497502
};
498503

499-
// The resource name of the access token.
500-
// Format: users/{user}/accessTokens/{access_token}
504+
// The resource name of the personal access token.
505+
// Format: users/{user}/personalAccessTokens/{personal_access_token}
501506
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
502507

503-
// Output only. The access token value.
504-
string access_token = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
508+
// The description of the token.
509+
string description = 2 [(google.api.field_behavior) = OPTIONAL];
505510

506-
// The description of the access token.
507-
string description = 3 [(google.api.field_behavior) = OPTIONAL];
508-
509-
// Output only. The issued timestamp.
510-
google.protobuf.Timestamp issued_at = 4 [(google.api.field_behavior) = OUTPUT_ONLY];
511+
// Output only. The creation timestamp.
512+
google.protobuf.Timestamp created_at = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
511513

512514
// Optional. The expiration timestamp.
513-
google.protobuf.Timestamp expires_at = 5 [(google.api.field_behavior) = OPTIONAL];
515+
google.protobuf.Timestamp expires_at = 4 [(google.api.field_behavior) = OPTIONAL];
516+
517+
// Output only. The last used timestamp.
518+
google.protobuf.Timestamp last_used_at = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
514519
}
515520

516-
message ListUserAccessTokensRequest {
517-
// Required. The parent resource whose access tokens will be listed.
521+
message ListPersonalAccessTokensRequest {
522+
// Required. The parent resource whose personal access tokens will be listed.
518523
// Format: users/{user}
519524
string parent = 1 [
520525
(google.api.field_behavior) = REQUIRED,
521526
(google.api.resource_reference) = {type: "memos.api.v1/User"}
522527
];
523528

524-
// Optional. The maximum number of access tokens to return.
529+
// Optional. The maximum number of tokens to return.
525530
int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];
526531

527532
// Optional. A page token for pagination.
528533
string page_token = 3 [(google.api.field_behavior) = OPTIONAL];
529534
}
530535

531-
message ListUserAccessTokensResponse {
532-
// The list of access tokens.
533-
repeated UserAccessToken access_tokens = 1;
536+
message ListPersonalAccessTokensResponse {
537+
// The list of personal access tokens.
538+
repeated PersonalAccessToken personal_access_tokens = 1;
534539

535540
// A token for the next page of results.
536541
string next_page_token = 2;
537542

538-
// The total count of access tokens.
543+
// The total count of personal access tokens.
539544
int32 total_size = 3;
540545
}
541546

542-
message CreateUserAccessTokenRequest {
543-
// Required. The parent resource where this access token will be created.
547+
message CreatePersonalAccessTokenRequest {
548+
// Required. The parent resource where this token will be created.
544549
// Format: users/{user}
545550
string parent = 1 [
546551
(google.api.field_behavior) = REQUIRED,
547552
(google.api.resource_reference) = {type: "memos.api.v1/User"}
548553
];
549554

550-
// Required. The access token to create.
551-
UserAccessToken access_token = 2 [(google.api.field_behavior) = REQUIRED];
555+
// Optional. Description of the personal access token.
556+
string description = 2 [(google.api.field_behavior) = OPTIONAL];
557+
558+
// Optional. Expiration duration in days (0 = never expires).
559+
int32 expires_in_days = 3 [(google.api.field_behavior) = OPTIONAL];
560+
}
561+
562+
message CreatePersonalAccessTokenResponse {
563+
// The personal access token metadata.
564+
PersonalAccessToken personal_access_token = 1;
552565

553-
// Optional. The access token ID to use.
554-
string access_token_id = 3 [(google.api.field_behavior) = OPTIONAL];
566+
// The actual token value - only returned on creation.
567+
// This is the only time the token value will be visible.
568+
string token = 2;
555569
}
556570

557-
message DeleteUserAccessTokenRequest {
558-
// Required. The resource name of the access token to delete.
559-
// Format: users/{user}/accessTokens/{access_token}
571+
message DeletePersonalAccessTokenRequest {
572+
// Required. The resource name of the personal access token to delete.
573+
// Format: users/{user}/personalAccessTokens/{personal_access_token}
560574
string name = 1 [
561575
(google.api.field_behavior) = REQUIRED,
562-
(google.api.resource_reference) = {type: "memos.api.v1/UserAccessToken"}
576+
(google.api.resource_reference) = {type: "memos.api.v1/PersonalAccessToken"}
563577
];
564578
}
565579

566-
message UserSession {
580+
// Session represents a user's login session on a specific device/browser.
581+
// Sessions are backed by refresh tokens with sliding expiration.
582+
message Session {
567583
option (google.api.resource) = {
568-
type: "memos.api.v1/UserSession"
584+
type: "memos.api.v1/Session"
569585
pattern: "users/{user}/sessions/{session}"
570586
name_field: "name"
571587
};
@@ -605,7 +621,7 @@ message UserSession {
605621
}
606622
}
607623

608-
message ListUserSessionsRequest {
624+
message ListSessionsRequest {
609625
// Required. The resource name of the parent.
610626
// Format: users/{user}
611627
string parent = 1 [
@@ -614,12 +630,12 @@ message ListUserSessionsRequest {
614630
];
615631
}
616632

617-
message ListUserSessionsResponse {
618-
// The list of user sessions.
619-
repeated UserSession sessions = 1;
633+
message ListSessionsResponse {
634+
// The list of sessions.
635+
repeated Session sessions = 1;
620636
}
621637

622-
message RevokeUserSessionRequest {
638+
message RevokeSessionRequest {
623639
// The name of the session to revoke.
624640
// Format: users/{user}/sessions/{session}
625641
string name = 1 [(google.api.field_behavior) = REQUIRED];

0 commit comments

Comments
 (0)