| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236 |
- package handler
- import (
- "bytes"
- "context"
- "net/http"
- "net/http/httptest"
- "net/url"
- "strings"
- "testing"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/ent/authidentity"
- "github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
- "github.com/Wei-Shaw/sub2api/ent/pendingauthsession"
- dbuser "github.com/Wei-Shaw/sub2api/ent/user"
- "github.com/Wei-Shaw/sub2api/internal/config"
- servermiddleware "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func TestSanitizeFrontendRedirectPath(t *testing.T) {
- require.Equal(t, "/dashboard", sanitizeFrontendRedirectPath("/dashboard"))
- require.Equal(t, "/dashboard", sanitizeFrontendRedirectPath(" /dashboard "))
- require.Equal(t, "", sanitizeFrontendRedirectPath("dashboard"))
- require.Equal(t, "", sanitizeFrontendRedirectPath("//evil.com"))
- require.Equal(t, "", sanitizeFrontendRedirectPath("https://evil.com"))
- require.Equal(t, "", sanitizeFrontendRedirectPath("/\nfoo"))
- long := "/" + strings.Repeat("a", linuxDoOAuthMaxRedirectLen)
- require.Equal(t, "", sanitizeFrontendRedirectPath(long))
- }
- func TestBuildBearerAuthorization(t *testing.T) {
- auth, err := buildBearerAuthorization("", "token123")
- require.NoError(t, err)
- require.Equal(t, "Bearer token123", auth)
- auth, err = buildBearerAuthorization("bearer", "token123")
- require.NoError(t, err)
- require.Equal(t, "Bearer token123", auth)
- _, err = buildBearerAuthorization("MAC", "token123")
- require.Error(t, err)
- _, err = buildBearerAuthorization("Bearer", "token 123")
- require.Error(t, err)
- }
- func TestLinuxDoParseUserInfoParsesIDAndUsername(t *testing.T) {
- cfg := config.LinuxDoConnectConfig{
- UserInfoURL: "https://connect.linux.do/api/user",
- }
- email, username, subject, displayName, avatarURL, err := linuxDoParseUserInfo(`{"id":123,"username":"alice","name":"Alice","avatar_url":"https://cdn.example/avatar.png"}`, cfg)
- require.NoError(t, err)
- require.Equal(t, "123", subject)
- require.Equal(t, "alice", username)
- require.Equal(t, "linuxdo-123@linuxdo-connect.invalid", email)
- require.Equal(t, "Alice", displayName)
- require.Equal(t, "https://cdn.example/avatar.png", avatarURL)
- }
- func TestLinuxDoParseUserInfoDefaultsUsername(t *testing.T) {
- cfg := config.LinuxDoConnectConfig{
- UserInfoURL: "https://connect.linux.do/api/user",
- }
- email, username, subject, displayName, avatarURL, err := linuxDoParseUserInfo(`{"id":"123"}`, cfg)
- require.NoError(t, err)
- require.Equal(t, "123", subject)
- require.Equal(t, "linuxdo_123", username)
- require.Equal(t, "linuxdo-123@linuxdo-connect.invalid", email)
- require.Equal(t, "linuxdo_123", displayName)
- require.Equal(t, "", avatarURL)
- }
- func TestLinuxDoParseUserInfoRejectsUnsafeSubject(t *testing.T) {
- cfg := config.LinuxDoConnectConfig{
- UserInfoURL: "https://connect.linux.do/api/user",
- }
- _, _, _, _, _, err := linuxDoParseUserInfo(`{"id":"123@456"}`, cfg)
- require.Error(t, err)
- tooLong := strings.Repeat("a", linuxDoOAuthMaxSubjectLen+1)
- _, _, _, _, _, err = linuxDoParseUserInfo(`{"id":"`+tooLong+`"}`, cfg)
- require.Error(t, err)
- }
- func TestParseOAuthProviderErrorJSON(t *testing.T) {
- code, desc := parseOAuthProviderError(`{"error":"invalid_client","error_description":"bad secret"}`)
- require.Equal(t, "invalid_client", code)
- require.Equal(t, "bad secret", desc)
- }
- func TestParseOAuthProviderErrorForm(t *testing.T) {
- code, desc := parseOAuthProviderError("error=invalid_request&error_description=Missing+code_verifier")
- require.Equal(t, "invalid_request", code)
- require.Equal(t, "Missing code_verifier", desc)
- }
- func TestParseLinuxDoTokenResponseJSON(t *testing.T) {
- token, ok := parseLinuxDoTokenResponse(`{"access_token":"t1","token_type":"Bearer","expires_in":3600,"scope":"user"}`)
- require.True(t, ok)
- require.Equal(t, "t1", token.AccessToken)
- require.Equal(t, "Bearer", token.TokenType)
- require.Equal(t, int64(3600), token.ExpiresIn)
- require.Equal(t, "user", token.Scope)
- }
- func TestParseLinuxDoTokenResponseForm(t *testing.T) {
- token, ok := parseLinuxDoTokenResponse("access_token=t2&token_type=bearer&expires_in=60")
- require.True(t, ok)
- require.Equal(t, "t2", token.AccessToken)
- require.Equal(t, "bearer", token.TokenType)
- require.Equal(t, int64(60), token.ExpiresIn)
- }
- func TestSingleLineStripsWhitespace(t *testing.T) {
- require.Equal(t, "hello world", singleLine("hello\r\nworld"))
- require.Equal(t, "", singleLine("\n\t\r"))
- }
- func TestLinuxDoOAuthBindStartRedirectsAndSetsBindCookies(t *testing.T) {
- handler := newLinuxDoOAuthTestHandler(t, false, config.LinuxDoConnectConfig{
- Enabled: true,
- ClientID: "linuxdo-client",
- ClientSecret: "linuxdo-secret",
- AuthorizeURL: "https://connect.linux.do/oauth/authorize",
- TokenURL: "https://connect.linux.do/oauth/token",
- UserInfoURL: "https://connect.linux.do/api/user",
- Scopes: "read",
- RedirectURL: "https://api.example.com/api/v1/auth/oauth/linuxdo/callback",
- FrontendRedirectURL: "/auth/linuxdo/callback",
- TokenAuthMethod: "client_secret_post",
- UsePKCE: true,
- })
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=/settings/connections", nil)
- c.Request = req
- c.Set(string(servermiddleware.ContextKeyUser), servermiddleware.AuthSubject{UserID: 42})
- handler.LinuxDoOAuthStart(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- location := recorder.Header().Get("Location")
- require.Contains(t, location, "connect.linux.do/oauth/authorize")
- require.Contains(t, location, "client_id=linuxdo-client")
- require.Contains(t, location, "code_challenge=")
- cookies := recorder.Result().Cookies()
- require.NotNil(t, findCookie(cookies, linuxDoOAuthStateCookieName))
- require.NotNil(t, findCookie(cookies, linuxDoOAuthRedirectCookie))
- require.NotNil(t, findCookie(cookies, linuxDoOAuthVerifierCookie))
- require.NotNil(t, findCookie(cookies, oauthPendingBrowserCookieName))
- intentCookie := findCookie(cookies, linuxDoOAuthIntentCookieName)
- require.NotNil(t, intentCookie)
- require.Equal(t, oauthIntentBindCurrentUser, decodeCookieValueForTest(t, intentCookie.Value))
- bindCookie := findCookie(cookies, linuxDoOAuthBindUserCookieName)
- require.NotNil(t, bindCookie)
- userID, err := parseOAuthBindUserCookieValue(decodeCookieValueForTest(t, bindCookie.Value), "test-secret")
- require.NoError(t, err)
- require.Equal(t, int64(42), userID)
- }
- func TestLinuxDoOAuthStartOmitsPKCEWhenDisabled(t *testing.T) {
- handler := newLinuxDoOAuthTestHandler(t, false, config.LinuxDoConnectConfig{
- Enabled: true,
- ClientID: "linuxdo-client",
- ClientSecret: "linuxdo-secret",
- AuthorizeURL: "https://connect.linux.do/oauth/authorize",
- TokenURL: "https://connect.linux.do/oauth/token",
- UserInfoURL: "https://connect.linux.do/api/user",
- Scopes: "read",
- RedirectURL: "https://api.example.com/api/v1/auth/oauth/linuxdo/callback",
- FrontendRedirectURL: "/auth/linuxdo/callback",
- TokenAuthMethod: "client_secret_post",
- UsePKCE: false,
- })
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/start?redirect=/dashboard", nil)
- handler.LinuxDoOAuthStart(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- require.NotContains(t, recorder.Header().Get("Location"), "code_challenge=")
- require.Nil(t, findCookie(recorder.Result().Cookies(), linuxDoOAuthVerifierCookie))
- }
- func TestLinuxDoOAuthCallbackAllowsMissingVerifierWhenPKCEDisabled(t *testing.T) {
- upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.URL.Path {
- case "/token":
- require.NoError(t, r.ParseForm())
- require.Empty(t, r.PostForm.Get("code_verifier"))
- 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":"compat-subject","username":"linuxdo_user","name":"LinuxDo Display"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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: false,
- })
- t.Cleanup(func() { _ = client.Close() })
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=linuxdo-code&state=state-123", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-123"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
- c.Request = req
- handler.LinuxDoOAuthCallback(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- location := recorder.Header().Get("Location")
- require.Contains(t, location, "/auth/linuxdo/callback#")
- require.Contains(t, location, "access_token=")
- requireCookieCleared(t, recorder, oauthPendingSessionCookieName)
- identity, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("linuxdo"),
- authidentity.ProviderKeyEQ("linuxdo"),
- authidentity.ProviderSubjectEQ("compat-subject"),
- ).
- Only(context.Background())
- require.NoError(t, err)
- require.Positive(t, identity.UserID)
- }
- func TestLinuxDoOAuthBindStartAcceptsAccessTokenCookie(t *testing.T) {
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, config.LinuxDoConnectConfig{
- Enabled: true,
- ClientID: "linuxdo-client",
- ClientSecret: "linuxdo-secret",
- AuthorizeURL: "https://connect.linux.do/oauth/authorize",
- TokenURL: "https://connect.linux.do/oauth/token",
- UserInfoURL: "https://connect.linux.do/api/user",
- 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() })
- user, err := client.User.Create().
- SetEmail("bind-cookie@example.com").
- SetUsername("bind-cookie-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(context.Background())
- require.NoError(t, err)
- token, err := handler.authService.GenerateToken(context.Background(), &service.User{
- ID: user.ID,
- Email: user.Email,
- Username: user.Username,
- PasswordHash: user.PasswordHash,
- Role: user.Role,
- Status: user.Status,
- })
- require.NoError(t, err)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/start?intent=bind_current_user&redirect=/settings/connections", nil)
- req.AddCookie(&http.Cookie{Name: oauthBindAccessTokenCookieName, Value: token, Path: oauthBindAccessTokenCookiePath})
- c.Request = req
- handler.LinuxDoOAuthStart(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- bindCookie := findCookie(recorder.Result().Cookies(), linuxDoOAuthBindUserCookieName)
- require.NotNil(t, bindCookie)
- userID, err := parseOAuthBindUserCookieValue(decodeCookieValueForTest(t, bindCookie.Value), "test-secret")
- require.NoError(t, err)
- require.Equal(t, user.ID, userID)
- accessTokenCookie := findCookie(recorder.Result().Cookies(), oauthBindAccessTokenCookieName)
- require.NotNil(t, accessTokenCookie)
- require.Equal(t, -1, accessTokenCookie.MaxAge)
- }
- func TestPrepareOAuthBindAccessTokenCookieSetsHttpOnlyCookie(t *testing.T) {
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, config.LinuxDoConnectConfig{})
- t.Cleanup(func() { _ = client.Close() })
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/bind-token", nil)
- req.Header.Set("Authorization", "Bearer access-token-value")
- c.Request = req
- handler.PrepareOAuthBindAccessTokenCookie(c)
- require.Equal(t, http.StatusNoContent, recorder.Code)
- accessTokenCookie := findCookie(recorder.Result().Cookies(), oauthBindAccessTokenCookieName)
- require.NotNil(t, accessTokenCookie)
- require.Equal(t, oauthBindAccessTokenCookiePath, accessTokenCookie.Path)
- require.Equal(t, linuxDoOAuthCookieMaxAgeSec, accessTokenCookie.MaxAge)
- require.True(t, accessTokenCookie.HttpOnly)
- require.Equal(t, url.QueryEscape("access-token-value"), accessTokenCookie.Value)
- }
- func TestLinuxDoOAuthCallbackCreatesLoginPendingSessionForExistingIdentityUser(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":"321","username":"linuxdo_user","name":"LinuxDo Display","avatar_url":"https://cdn.example/linuxdo.png"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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() })
- ctx := context.Background()
- existingUser, err := client.User.Create().
- SetEmail(linuxDoSyntheticEmail("321")).
- SetUsername("legacy-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.AuthIdentity.Create().
- SetUserID(existingUser.ID).
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("321").
- SetMetadata(map[string]any{"username": "legacy-user"}).
- Save(ctx)
- require.NoError(t, err)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=code-123&state=state-123", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-123"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-123"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-123"))
- 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)
- 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.NotNil(t, session.TargetUserID)
- require.Equal(t, existingUser.ID, *session.TargetUserID)
- require.Equal(t, linuxDoSyntheticEmail("321"), session.ResolvedEmail)
- require.Equal(t, "LinuxDo Display", session.UpstreamIdentityClaims["suggested_display_name"])
- completion, ok := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
- require.True(t, ok)
- require.Equal(t, "/dashboard", completion["redirect"])
- _, hasAccessToken := completion["access_token"]
- require.False(t, hasAccessToken)
- _, hasRefreshToken := completion["refresh_token"]
- require.False(t, hasRefreshToken)
- require.Nil(t, completion["error"])
- }
- func TestLinuxDoOAuthCallbackRejectsDisabledExistingIdentityUser(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":"654","username":"linuxdo_disabled","name":"LinuxDo Disabled"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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() })
- ctx := context.Background()
- existingUser, err := client.User.Create().
- SetEmail(linuxDoSyntheticEmail("654")).
- SetUsername("disabled-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusDisabled).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.AuthIdentity.Create().
- SetUserID(existingUser.ID).
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("654").
- Save(ctx)
- require.NoError(t, err)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=code-disabled&state=state-disabled", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-disabled"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-disabled"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-disabled"))
- c.Request = req
- handler.LinuxDoOAuthCallback(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- require.Nil(t, findCookie(recorder.Result().Cookies(), oauthPendingSessionCookieName))
- assertOAuthRedirectError(t, recorder.Header().Get("Location"), "session_error", "USER_NOT_ACTIVE")
- count, err := client.PendingAuthSession.Query().Count(ctx)
- require.NoError(t, err)
- require.Zero(t, count)
- }
- func TestLinuxDoOAuthCallbackCreatesBindPendingSessionForCompatEmailUser(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":"321","email":"legacy@example.com","username":"linuxdo_user","name":"LinuxDo Display","avatar_url":"https://cdn.example/linuxdo.png"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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() })
- ctx := context.Background()
- existingUser, err := client.User.Create().
- SetEmail(" Legacy@Example.com ").
- SetUsername("legacy-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=code-compat&state=state-compat", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-compat"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-compat"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-compat"))
- 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)
- 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.NotNil(t, session.TargetUserID)
- require.Equal(t, existingUser.ID, *session.TargetUserID)
- require.Equal(t, strings.TrimSpace(existingUser.Email), session.ResolvedEmail)
- require.Equal(t, "legacy@example.com", session.UpstreamIdentityClaims["compat_email"])
- completion, ok := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
- require.True(t, ok)
- require.Equal(t, "/dashboard", completion["redirect"])
- require.Equal(t, oauthPendingChoiceStep, completion["step"])
- require.Equal(t, strings.TrimSpace(existingUser.Email), completion["email"])
- require.Equal(t, strings.TrimSpace(existingUser.Email), completion["existing_account_email"])
- require.Equal(t, true, completion["existing_account_bindable"])
- require.Equal(t, "compat_email_match", completion["choice_reason"])
- _, hasAccessToken := completion["access_token"]
- require.False(t, hasAccessToken)
- }
- func TestLinuxDoOAuthCallbackCreatesChoicePendingSessionWhenSignupRequiresInvite(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":"654","username":"linuxdo_invite","name":"Need Invite","avatar_url":"https://cdn.example/invite.png"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, true, 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-456&state=state-456", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-456"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-456"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-456"))
- 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)
- completion, ok := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
- require.True(t, ok)
- require.Equal(t, oauthPendingChoiceStep, completion["step"])
- require.Equal(t, "/dashboard", completion["redirect"])
- 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 {
- 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":"direct-123","username":"linuxdo_direct","name":"Direct Login","avatar_url":"https://cdn.example/direct.png"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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-direct&state=state-direct", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-direct"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/dashboard"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-direct"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentLogin))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-direct"))
- c.Request = req
- handler.LinuxDoOAuthCallback(c)
- require.Equal(t, http.StatusFound, recorder.Code)
- location := recorder.Header().Get("Location")
- require.Contains(t, location, "/auth/linuxdo/callback#")
- require.Contains(t, location, "access_token=")
- require.Contains(t, location, "refresh_token=")
- fragmentValues := parseOAuthRedirectFragment(t, location)
- require.Equal(t, "/dashboard", fragmentValues.Get("redirect"))
- requireCookieCleared(t, recorder, oauthPendingSessionCookieName)
- requireCookieCleared(t, recorder, oauthPendingBrowserCookieName)
- ctx := context.Background()
- userEntity, err := client.User.Query().
- Where(dbuser.EmailEQ("linuxdo-direct-123@linuxdo-connect.invalid")).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, "linuxdo_direct", userEntity.Username)
- require.Equal(t, "linuxdo", userEntity.SignupSource)
- identity, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("linuxdo"),
- authidentity.ProviderKeyEQ("linuxdo"),
- authidentity.ProviderSubjectEQ("direct-123"),
- ).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, userEntity.ID, identity.UserID)
- require.Equal(t, "https://cdn.example/direct.png", identity.Metadata["suggested_avatar_url"])
- sessionCount, err := client.PendingAuthSession.Query().Count(ctx)
- require.NoError(t, err)
- require.Zero(t, sessionCount)
- }
- func TestLinuxDoOAuthCallbackCreatesBindPendingSessionForCurrentUser(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":"999","username":"bind_user","name":"Bind Display","avatar_url":"https://cdn.example/bind.png"}`))
- default:
- http.NotFound(w, r)
- }
- }))
- defer upstream.Close()
- handler, client := newLinuxDoOAuthHandlerAndClient(t, false, 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() })
- ctx := context.Background()
- currentUser, err := client.User.Create().
- SetEmail("current@example.com").
- SetUsername("current-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/linuxdo/callback?code=code-bind&state=state-bind", nil)
- req.AddCookie(encodedCookie(linuxDoOAuthStateCookieName, "state-bind"))
- req.AddCookie(encodedCookie(linuxDoOAuthRedirectCookie, "/settings/connections"))
- req.AddCookie(encodedCookie(linuxDoOAuthVerifierCookie, "verifier-bind"))
- req.AddCookie(encodedCookie(linuxDoOAuthIntentCookieName, oauthIntentBindCurrentUser))
- req.AddCookie(encodedCookie(linuxDoOAuthBindUserCookieName, buildEncodedOAuthBindUserCookie(t, currentUser.ID, "test-secret")))
- req.AddCookie(encodedCookie(oauthPendingBrowserCookieName, "browser-bind"))
- 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)
- session, err := client.PendingAuthSession.Query().
- Where(pendingauthsession.SessionTokenEQ(decodeCookieValueForTest(t, sessionCookie.Value))).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, oauthIntentBindCurrentUser, session.Intent)
- require.NotNil(t, session.TargetUserID)
- require.Equal(t, currentUser.ID, *session.TargetUserID)
- require.Equal(t, linuxDoSyntheticEmail("999"), session.ResolvedEmail)
- completion, ok := session.LocalFlowState[oauthCompletionResponseKey].(map[string]any)
- require.True(t, ok)
- require.Equal(t, "/settings/connections", completion["redirect"])
- require.Empty(t, completion["access_token"])
- require.Equal(t, "Bind Display", session.UpstreamIdentityClaims["suggested_display_name"])
- userCount, err := client.User.Query().Count(ctx)
- require.NoError(t, err)
- require.Equal(t, 1, userCount)
- }
- func TestCompleteLinuxDoOAuthRegistrationAppliesPendingAdoptionDecision(t *testing.T) {
- handler, client := newOAuthPendingFlowTestHandler(t, false)
- ctx := context.Background()
- session, err := client.PendingAuthSession.Create().
- SetSessionToken("linuxdo-complete-session").
- SetIntent("login").
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-subject-1").
- SetResolvedEmail("linuxdo-subject-1@linuxdo-connect.invalid").
- SetBrowserSessionKey("linuxdo-browser").
- SetUpstreamIdentityClaims(map[string]any{
- "username": "linuxdo_user",
- "suggested_display_name": "LinuxDo Display",
- "suggested_avatar_url": "https://cdn.example/linuxdo.png",
- }).
- SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
- Save(ctx)
- require.NoError(t, err)
- _, err = service.NewAuthPendingIdentityService(client).UpsertAdoptionDecision(ctx, service.PendingIdentityAdoptionDecisionInput{
- PendingAuthSessionID: session.ID,
- AdoptAvatar: true,
- })
- require.NoError(t, err)
- body := bytes.NewBufferString(`{"invitation_code":"invite-1","adopt_display_name":true}`)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/linuxdo/complete-registration", body)
- req.Header.Set("Content-Type", "application/json")
- req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
- req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("linuxdo-browser")})
- c.Request = req
- handler.CompleteLinuxDoOAuthRegistration(c)
- require.Equal(t, http.StatusOK, recorder.Code)
- responseData := decodeJSONBody(t, recorder)
- require.NotEmpty(t, responseData["access_token"])
- userEntity, err := client.User.Query().
- Where(dbuser.EmailEQ(session.ResolvedEmail)).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, "LinuxDo Display", userEntity.Username)
- identity, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("linuxdo"),
- authidentity.ProviderKeyEQ("linuxdo"),
- authidentity.ProviderSubjectEQ("linuxdo-subject-1"),
- ).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, userEntity.ID, identity.UserID)
- require.Equal(t, "LinuxDo Display", identity.Metadata["display_name"])
- require.Equal(t, "https://cdn.example/linuxdo.png", identity.Metadata["avatar_url"])
- decision, err := client.IdentityAdoptionDecision.Query().
- Where(identityadoptiondecision.PendingAuthSessionIDEQ(session.ID)).
- Only(ctx)
- require.NoError(t, err)
- require.NotNil(t, decision.IdentityID)
- require.Equal(t, identity.ID, *decision.IdentityID)
- require.True(t, decision.AdoptDisplayName)
- require.True(t, decision.AdoptAvatar)
- consumed, err := client.PendingAuthSession.Query().
- Where(pendingauthsession.IDEQ(session.ID)).
- Only(ctx)
- require.NoError(t, err)
- require.NotNil(t, consumed.ConsumedAt)
- }
- func TestCompleteLinuxDoOAuthRegistrationRejectsAdoptExistingUserSession(t *testing.T) {
- handler, client := newOAuthPendingFlowTestHandler(t, false)
- ctx := context.Background()
- existingUser, err := client.User.Create().
- SetEmail("owner@example.com").
- SetUsername("owner-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- session, err := client.PendingAuthSession.Create().
- SetSessionToken("linuxdo-complete-invalid-session").
- SetIntent("adopt_existing_user_by_email").
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-invalid-subject-1").
- SetTargetUserID(existingUser.ID).
- SetResolvedEmail(existingUser.Email).
- SetBrowserSessionKey("linuxdo-invalid-browser").
- SetUpstreamIdentityClaims(map[string]any{
- "username": "linuxdo_user",
- }).
- SetLocalFlowState(map[string]any{
- oauthCompletionResponseKey: map[string]any{
- "step": "bind_login_required",
- },
- }).
- SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
- Save(ctx)
- require.NoError(t, err)
- body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/linuxdo/complete-registration", body)
- req.Header.Set("Content-Type", "application/json")
- req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
- req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("linuxdo-invalid-browser")})
- c.Request = req
- handler.CompleteLinuxDoOAuthRegistration(c)
- require.Equal(t, http.StatusBadRequest, recorder.Code)
- storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
- require.NoError(t, err)
- require.Nil(t, storedSession.ConsumedAt)
- }
- func TestCompleteLinuxDoOAuthRegistrationReturnsPendingSessionWhenChoiceStillRequired(t *testing.T) {
- handler, client := newOAuthPendingFlowTestHandler(t, false)
- ctx := context.Background()
- session, err := client.PendingAuthSession.Create().
- SetSessionToken("linuxdo-complete-choice-session").
- SetIntent("login").
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-choice-subject-1").
- SetResolvedEmail("linuxdo-choice-subject-1@linuxdo-connect.invalid").
- SetBrowserSessionKey("linuxdo-choice-browser").
- SetUpstreamIdentityClaims(map[string]any{
- "username": "linuxdo_user",
- }).
- SetLocalFlowState(map[string]any{
- oauthCompletionResponseKey: map[string]any{
- "step": oauthPendingChoiceStep,
- "redirect": "/dashboard",
- "email": "fresh@example.com",
- "resolved_email": "fresh@example.com",
- "force_email_on_signup": true,
- },
- }).
- SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
- Save(ctx)
- require.NoError(t, err)
- body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/linuxdo/complete-registration", body)
- req.Header.Set("Content-Type", "application/json")
- req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
- req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("linuxdo-choice-browser")})
- c.Request = req
- handler.CompleteLinuxDoOAuthRegistration(c)
- require.Equal(t, http.StatusOK, recorder.Code)
- responseData := decodeJSONBody(t, recorder)
- require.Equal(t, "pending_session", responseData["auth_result"])
- require.Equal(t, oauthPendingChoiceStep, responseData["step"])
- require.Equal(t, true, responseData["force_email_on_signup"])
- require.Empty(t, responseData["access_token"])
- userCount, err := client.User.Query().Count(ctx)
- require.NoError(t, err)
- require.Zero(t, userCount)
- storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
- require.NoError(t, err)
- require.Nil(t, storedSession.ConsumedAt)
- }
- func TestCompleteLinuxDoOAuthRegistrationBindsIdentityWithoutAdoptionFlags(t *testing.T) {
- handler, client := newOAuthPendingFlowTestHandler(t, false)
- ctx := context.Background()
- session, err := client.PendingAuthSession.Create().
- SetSessionToken("linuxdo-complete-no-adoption-session").
- SetIntent("login").
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-subject-no-adoption").
- SetResolvedEmail("linuxdo-subject-no-adoption@linuxdo-connect.invalid").
- SetBrowserSessionKey("linuxdo-browser-no-adoption").
- SetUpstreamIdentityClaims(map[string]any{
- "username": "linuxdo_user",
- "suggested_display_name": "LinuxDo Legacy",
- "suggested_avatar_url": "https://cdn.example/linuxdo-legacy.png",
- }).
- SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
- Save(ctx)
- require.NoError(t, err)
- body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/linuxdo/complete-registration", body)
- req.Header.Set("Content-Type", "application/json")
- req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
- req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("linuxdo-browser-no-adoption")})
- c.Request = req
- handler.CompleteLinuxDoOAuthRegistration(c)
- require.Equal(t, http.StatusOK, recorder.Code)
- responseData := decodeJSONBody(t, recorder)
- require.NotEmpty(t, responseData["access_token"])
- require.NotEmpty(t, responseData["refresh_token"])
- userEntity, err := client.User.Query().
- Where(dbuser.EmailEQ(session.ResolvedEmail)).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, "linuxdo_user", userEntity.Username)
- identity, err := client.AuthIdentity.Query().
- Where(
- authidentity.ProviderTypeEQ("linuxdo"),
- authidentity.ProviderKeyEQ("linuxdo"),
- authidentity.ProviderSubjectEQ("linuxdo-subject-no-adoption"),
- ).
- Only(ctx)
- require.NoError(t, err)
- require.Equal(t, userEntity.ID, identity.UserID)
- decision, err := client.IdentityAdoptionDecision.Query().
- Where(identityadoptiondecision.PendingAuthSessionIDEQ(session.ID)).
- Only(ctx)
- require.NoError(t, err)
- require.NotNil(t, decision.IdentityID)
- require.Equal(t, identity.ID, *decision.IdentityID)
- require.False(t, decision.AdoptDisplayName)
- require.False(t, decision.AdoptAvatar)
- }
- func TestCompleteLinuxDoOAuthRegistrationRejectsIdentityOwnershipConflictBeforeUserCreation(t *testing.T) {
- handler, client := newOAuthPendingFlowTestHandler(t, false)
- ctx := context.Background()
- existingOwner, err := client.User.Create().
- SetEmail("owner@example.com").
- SetUsername("owner-user").
- SetPasswordHash("hash").
- SetRole(service.RoleUser).
- SetStatus(service.StatusActive).
- Save(ctx)
- require.NoError(t, err)
- _, err = client.AuthIdentity.Create().
- SetUserID(existingOwner.ID).
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-conflict-subject").
- Save(ctx)
- require.NoError(t, err)
- session, err := client.PendingAuthSession.Create().
- SetSessionToken("linuxdo-complete-conflict-session").
- SetIntent("login").
- SetProviderType("linuxdo").
- SetProviderKey("linuxdo").
- SetProviderSubject("linuxdo-conflict-subject").
- SetResolvedEmail("linuxdo-conflict-subject@linuxdo-connect.invalid").
- SetBrowserSessionKey("linuxdo-conflict-browser").
- SetUpstreamIdentityClaims(map[string]any{
- "username": "linuxdo_user",
- }).
- SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
- Save(ctx)
- require.NoError(t, err)
- body := bytes.NewBufferString(`{"invitation_code":"invite-1"}`)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/linuxdo/complete-registration", body)
- req.Header.Set("Content-Type", "application/json")
- req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
- req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue("linuxdo-conflict-browser")})
- c.Request = req
- handler.CompleteLinuxDoOAuthRegistration(c)
- require.Equal(t, http.StatusConflict, recorder.Code)
- payload := decodeJSONBody(t, recorder)
- require.Equal(t, "AUTH_IDENTITY_OWNERSHIP_CONFLICT", payload["reason"])
- userCount, err := client.User.Query().
- Where(dbuser.EmailEQ("linuxdo-conflict-subject@linuxdo-connect.invalid")).
- Count(ctx)
- require.NoError(t, err)
- require.Zero(t, userCount)
- storedSession, err := client.PendingAuthSession.Get(ctx, session.ID)
- require.NoError(t, err)
- require.Nil(t, storedSession.ConsumedAt)
- }
- func newLinuxDoOAuthTestHandler(t *testing.T, invitationEnabled bool, oauthCfg config.LinuxDoConnectConfig) *AuthHandler {
- t.Helper()
- handler, _ := newLinuxDoOAuthHandlerAndClient(t, invitationEnabled, oauthCfg)
- return handler
- }
- 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{
- Secret: "test-secret",
- ExpireHour: 1,
- AccessTokenExpireMinutes: 60,
- RefreshTokenExpireDays: 7,
- },
- LinuxDo: oauthCfg,
- }
- }
|