mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3560 from StarryKira/feat/issue-3557-oauth-email
feat: fix OAuth email completion flow
This commit is contained in:
@@ -383,6 +383,7 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
|
||||
upstreamClaims,
|
||||
compatEmail,
|
||||
compatEmailUser,
|
||||
emailVerificationRequired,
|
||||
forceEmailOnSignup,
|
||||
); err != nil {
|
||||
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
|
||||
@@ -433,6 +434,7 @@ func (h *AuthHandler) createLinuxDoOAuthChoicePendingSession(
|
||||
upstreamClaims map[string]any,
|
||||
compatEmail string,
|
||||
compatEmailUser *dbent.User,
|
||||
emailVerificationRequired bool,
|
||||
forceEmailOnSignup bool,
|
||||
) error {
|
||||
suggestionEmail := strings.TrimSpace(suggestedEmail)
|
||||
@@ -467,6 +469,17 @@ func (h *AuthHandler) createLinuxDoOAuthChoicePendingSession(
|
||||
if forceEmailOnSignup && compatEmailUser == nil {
|
||||
completionResponse["choice_reason"] = "force_email_on_signup"
|
||||
}
|
||||
if (emailVerificationRequired || forceEmailOnSignup) && compatEmailUser == nil {
|
||||
completionResponse["step"] = "create_account_required"
|
||||
completionResponse["email_binding_required"] = true
|
||||
completionResponse["force_email_on_signup"] = true
|
||||
if emailVerificationRequired {
|
||||
completionResponse["choice_reason"] = "email_verification_required"
|
||||
}
|
||||
delete(completionResponse, "email")
|
||||
delete(completionResponse, "resolved_email")
|
||||
resolvedChoiceEmail = ""
|
||||
}
|
||||
|
||||
var targetUserID *int64
|
||||
if compatEmailUser != nil && compatEmailUser.ID > 0 {
|
||||
|
||||
@@ -631,6 +631,108 @@ func TestLinuxDoOAuthCallbackCreatesChoicePendingSessionWhenSignupRequiresInvite
|
||||
require.Equal(t, "third_party_signup", completion["choice_reason"])
|
||||
}
|
||||
|
||||
func TestLinuxDoOAuthCallbackEmailVerificationCompletesWithBoundEmail(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"access_token":"linuxdo-access","token_type":"Bearer","expires_in":3600}`))
|
||||
case "/userinfo":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"id":"email-verify-123","username":"linuxdo_email","name":"Email Verify","avatar_url":"https://cdn.example/email.png"}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
handler, client := newLinuxDoOAuthHandlerAndClientWithEmailVerification(t, false, "fresh@example.com", "246810", config.LinuxDoConnectConfig{
|
||||
Enabled: true,
|
||||
ClientID: "linuxdo-client",
|
||||
ClientSecret: "linuxdo-secret",
|
||||
AuthorizeURL: upstream.URL + "/authorize",
|
||||
TokenURL: upstream.URL + "/token",
|
||||
UserInfoURL: upstream.URL + "/userinfo",
|
||||
Scopes: "read",
|
||||
RedirectURL: "https://api.example.com/api/v1/auth/oauth/linuxdo/callback",
|
||||
FrontendRedirectURL: "/auth/linuxdo/callback",
|
||||
TokenAuthMethod: "client_secret_post",
|
||||
UsePKCE: true,
|
||||
})
|
||||
t.Cleanup(func() { _ = client.Close() })
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=code-email&state=state-email", nil)
|
||||
req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-email"))
|
||||
req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
|
||||
req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-email"))
|
||||
req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
|
||||
req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-email"))
|
||||
c.Request = req
|
||||
|
||||
handler.LinuxDoOAuthCallback(c)
|
||||
|
||||
require.Equal(t, http.StatusFound, recorder.Code)
|
||||
require.Equal(t, "/auth/linuxdo/callback", recorder.Header().Get("Location"))
|
||||
sessionCookie := findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName)
|
||||
require.NotNil(t, sessionCookie)
|
||||
|
||||
ctx := context.Background()
|
||||
session, err := client.PendingAuthSession.Query().
|
||||
Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
|
||||
Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, oauthIntentLogin, session.Intent)
|
||||
require.Nil(t, session.TargetUserID)
|
||||
require.Empty(t, session.ResolvedEmail)
|
||||
require.Equal(t, "linuxdo-email-verify-123@linuxdo-connect.invalid", session.UpstreamIdentityClaims["email"])
|
||||
|
||||
completion, ok := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "create_account_required", completion["step"])
|
||||
require.Equal(t, true, completion["email_binding_required"])
|
||||
require.Equal(t, true, completion["force_email_on_signup"])
|
||||
require.Equal(t, "email_verification_required", completion["choice_reason"])
|
||||
require.NotContains(t, completion, "email")
|
||||
require.NotContains(t, completion, "resolved_email")
|
||||
|
||||
createRecorder := httptest.NewRecorder()
|
||||
createCtx, _ := gin.CreateTestContext(createRecorder)
|
||||
body := bytes.NewBufferString(`{"email":"fresh@example.com","verify_code":"246810","password":"secret-123","adopt_display_name":false,"adopt_avatar":false}`)
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/pending/create-account", body)
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.AddCookie(sessionCookie)
|
||||
createReq.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("browser-email")})
|
||||
createCtx.Request = createReq
|
||||
|
||||
handler.CreatePendingOAuthAccount(createCtx)
|
||||
|
||||
require.Equal(t, http.StatusOK, createRecorder.Code)
|
||||
responseData := decodeJSONBody(t, createRecorder)
|
||||
require.NotEmpty(t, responseData["access_token"])
|
||||
|
||||
userEntity, err := client.User.Query().
|
||||
Where(dbuser.EmailEQ("fresh@example.com")).
|
||||
Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "linuxdo", userEntity.SignupSource)
|
||||
|
||||
identity, err := client.AuthIdentity.Query().
|
||||
Where(
|
||||
authidentity.ProviderTypeEQ("linuxdo"),
|
||||
authidentity.ProviderKeyEQ("linuxdo"),
|
||||
authidentity.ProviderSubjectEQ("email-verify-123"),
|
||||
).
|
||||
Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userEntity.ID, identity.UserID)
|
||||
|
||||
storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, storedSession.ConsumedAt)
|
||||
}
|
||||
|
||||
func TestLinuxDoOAuthCallbackDirectlyLogsInNewUserWhenEmailVerificationDisabled(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -1103,6 +1205,24 @@ func newLinuxDoOAuthTestHandler(t *testing.T, invitationEnabled bool, oauthCfg c
|
||||
func newLinuxDoOAuthHandlerAndClient(t *testing.T, invitationEnabled bool, oauthCfg config.LinuxDoConnectConfig) (*AuthHandler, *dbent.Client) {
|
||||
t.Helper()
|
||||
handler, client := newOAuthPendingFlowTestHandler(t, invitationEnabled)
|
||||
configureLinuxDoOAuthTestHandler(handler, oauthCfg)
|
||||
return handler, client
|
||||
}
|
||||
|
||||
func newLinuxDoOAuthHandlerAndClientWithEmailVerification(
|
||||
t *testing.T,
|
||||
invitationEnabled bool,
|
||||
email string,
|
||||
code string,
|
||||
oauthCfg config.LinuxDoConnectConfig,
|
||||
) (*AuthHandler, *dbent.Client) {
|
||||
t.Helper()
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithEmailVerification(t, invitationEnabled, email, code)
|
||||
configureLinuxDoOAuthTestHandler(handler, oauthCfg)
|
||||
return handler, client
|
||||
}
|
||||
|
||||
func configureLinuxDoOAuthTestHandler(handler *AuthHandler, oauthCfg config.LinuxDoConnectConfig) {
|
||||
handler.settingSvc = nil
|
||||
handler.cfg = &config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
@@ -1113,5 +1233,4 @@ func newLinuxDoOAuthHandlerAndClient(t *testing.T, invitationEnabled bool, oauth
|
||||
},
|
||||
LinuxDo: oauthCfg,
|
||||
}
|
||||
return handler, client
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ func backendModeAllowsAuthPath(path string) bool {
|
||||
"/auth/oauth/github/callback",
|
||||
"/auth/oauth/google/callback",
|
||||
"/auth/oauth/dingtalk/callback",
|
||||
"/auth/oauth/github/complete-registration",
|
||||
"/auth/oauth/google/complete-registration",
|
||||
"/auth/oauth/linuxdo/complete-registration",
|
||||
"/auth/oauth/wechat/complete-registration",
|
||||
"/auth/oauth/oidc/complete-registration",
|
||||
|
||||
@@ -258,6 +258,12 @@ func TestBackendModeAuthGuard(t *testing.T) {
|
||||
path: "/api/v1/auth/oauth/github/callback",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "enabled_allows_github_complete_registration",
|
||||
enabled: "true",
|
||||
path: "/api/v1/auth/oauth/github/complete-registration",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "enabled_blocks_google_oauth_start",
|
||||
enabled: "true",
|
||||
@@ -270,6 +276,12 @@ func TestBackendModeAuthGuard(t *testing.T) {
|
||||
path: "/api/v1/auth/oauth/google/callback",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "enabled_allows_google_complete_registration",
|
||||
enabled: "true",
|
||||
path: "/api/v1/auth/oauth/google/complete-registration",
|
||||
wantStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "enabled_blocks_dingtalk_oauth_start",
|
||||
enabled: "true",
|
||||
|
||||
Reference in New Issue
Block a user