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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,11 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re
return terr
}
} else {
createdUser = true
if _, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil {
var decision models.AccountLinkingDecision
if decision, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil {
return terr
}
createdUser = decision == models.CreateAccount
}
if flowState != nil && flowState.IsPKCE() {
// PKCE flow: update flow state with user ID and tokens
Expand Down
36 changes: 36 additions & 0 deletions internal/api/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -323,6 +324,41 @@ func assertValidOAuthState(ts *ExternalTestSuite, state string, expectedProvider
ts.Equal(expectedProvider, flowState.ProviderType, "flow state provider should match")
}

// after-user-created must fire once when the external identity creates an
// account, and not again when the same user signs in later.
func (ts *ExternalTestSuite) TestExternalCallbackAfterUserCreatedHookOnlyOnSignup() {
code := "authcode"
emails := `[{"email":"github-after-hook@example.com", "primary": true, "verified": true}]`
tokenCount, userCount := 0, 0
server := GitHubTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

var hookCalls int32
hookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&hookCalls, 1)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, "{}")
}))
defer hookServer.Close()

originalHook := ts.Config.Hook.AfterUserCreated
ts.Config.Hook.AfterUserCreated = conf.ExtensibilityPointConfiguration{
Enabled: true,
URI: hookServer.URL,
}
defer func() { ts.Config.Hook.AfterUserCreated = originalHook }()

// First sign-in creates the account -> hook fires once.
performAuthorization(ts, "github", code, "")
require.Equal(ts.T(), int32(1), atomic.LoadInt32(&hookCalls),
"after-user-created hook should fire once when the account is created")

// Second sign-in with the same identity is a returning-user login -> hook must not fire again.
performAuthorization(ts, "github", code, "")
require.Equal(ts.T(), int32(1), atomic.LoadInt32(&hookCalls),
"after-user-created hook must not fire on subsequent external sign-ins")
}

// TestSignupExternalUnsupported tests API /authorize for an unsupported external provider
func (ts *ExternalTestSuite) TestSignupExternalUnsupported() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=external", nil)
Expand Down