Skip to content

Commit e67a990

Browse files
Saancreedjakubfijalkowski
authored andcommitted
feat: redirect to OIDC providers only once in registration flows
test(e2e): ensure there is only one OIDC redirect Co-authored-by: Jakub Fijałkowski <[email protected]>
1 parent f1349ba commit e67a990

File tree

3 files changed

+108
-5
lines changed

3 files changed

+108
-5
lines changed

selfservice/strategy/oidc/strategy_registration.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ var jsonnetCache, _ = ristretto.NewCache(&ristretto.Config[[]byte, []byte]{
4343

4444
type MetadataType string
4545

46+
type OIDCProviderData struct {
47+
Provider string `json:"provider"`
48+
Tokens *identity.CredentialsOIDCEncryptedTokens `json:"tokens"`
49+
Claims Claims `json:"claims"`
50+
}
51+
4652
type VerifiedAddress struct {
4753
Value string `json:"value"`
4854
Via identity.VerifiableAddressType `json:"via"`
@@ -53,6 +59,8 @@ const (
5359

5460
PublicMetadata MetadataType = "identity.metadata_public"
5561
AdminMetadata MetadataType = "identity.metadata_admin"
62+
63+
InternalContextKeyProviderData = "provider_data"
5664
)
5765

5866
func (s *Strategy) RegisterRegistrationRoutes(r *x.RouterPublic) {
@@ -216,6 +224,27 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat
216224
return errors.WithStack(flow.ErrCompletedByStrategy)
217225
}
218226

227+
providerDataKey := flow.PrefixInternalContextKey(s.ID(), InternalContextKeyProviderData)
228+
if oidcProviderData := gjson.GetBytes(f.InternalContext, providerDataKey); oidcProviderData.IsObject() {
229+
var providerData OIDCProviderData
230+
if err = json.Unmarshal([]byte(oidcProviderData.Raw), &providerData); err != nil {
231+
return s.handleError(ctx, w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected OIDC provider data in internal context to be an object but got: %s", err)))
232+
}
233+
if pid != providerData.Provider {
234+
return s.handleError(ctx, w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected OIDC provider data in internal context to have matching provider but got: %s", providerData.Provider)))
235+
}
236+
container := &AuthCodeContainer{
237+
FlowID: f.ID.String(),
238+
Traits: p.Traits,
239+
TransientPayload: f.TransientPayload,
240+
}
241+
_, err = s.processRegistration(ctx, w, r, f, providerData.Tokens, &providerData.Claims, provider, container)
242+
if err != nil {
243+
return s.handleError(ctx, w, r, f, pid, container.Traits, err)
244+
}
245+
return errors.WithStack(flow.ErrCompletedByStrategy)
246+
}
247+
219248
state, pkce, err := s.GenerateState(ctx, provider, f.ID)
220249
if err != nil {
221250
return s.handleError(ctx, w, r, f, pid, nil, err)
@@ -313,6 +342,13 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite
313342
return nil, nil
314343
}
315344

345+
providerDataKey := flow.PrefixInternalContextKey(s.ID(), InternalContextKeyProviderData)
346+
if hasOIDCProviderData := gjson.GetBytes(rf.InternalContext, providerDataKey).IsObject(); !hasOIDCProviderData {
347+
if internalContext, err := sjson.SetBytes(rf.InternalContext, providerDataKey, &OIDCProviderData{Provider: provider.Config().ID, Tokens: token, Claims: *claims}); err == nil {
348+
rf.InternalContext = internalContext
349+
}
350+
}
351+
316352
fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(ctx)), fetcher.WithCache(jsonnetCache, 60*time.Minute))
317353
jsonnetMapperSnippet, err := fetch.FetchContext(ctx, provider.Config().Mapper)
318354
if err != nil {
@@ -351,6 +387,10 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite
351387
return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err)
352388
}
353389

390+
if internalContext, err := sjson.DeleteBytes(rf.InternalContext, providerDataKey); err == nil {
391+
rf.InternalContext = internalContext
392+
}
393+
354394
return nil, nil
355395
}
356396

test/e2e/cypress/integration/profiles/oidc/registration/success.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,56 @@ context("Social Sign Up Successes", () => {
9494
cy.triggerOidc(app)
9595

9696
cy.location("pathname").should((loc) => {
97-
expect(loc).to.be.oneOf(["/welcome", "/", "/sessions"])
97+
expect(loc).to.be.oneOf([
98+
"/welcome",
99+
"/",
100+
"/sessions",
101+
"/verification",
102+
])
103+
})
104+
105+
cy.getSession().should((session) => {
106+
shouldSession(email)(session)
107+
expect(session.identity.traits.consent).to.equal(true)
108+
})
109+
})
110+
111+
it("should redirect to oidc provider only once", () => {
112+
const email = gen.email()
113+
114+
cy.registerOidc({
115+
app,
116+
email,
117+
expectSession: false,
118+
route: registration,
119+
})
120+
121+
cy.get(appPrefix(app) + '[name="traits.email"]').should(
122+
"have.value",
123+
email,
124+
)
125+
126+
cy.get('[name="traits.consent"][type="checkbox"]')
127+
.siblings("label")
128+
.click()
129+
cy.get('[name="traits.newsletter"][type="checkbox"]')
130+
.siblings("label")
131+
.click()
132+
cy.get('[name="traits.website"]').type(website)
133+
134+
cy.intercept("GET", "http://*/oauth2/auth*").as("additionalRedirect")
135+
136+
cy.triggerOidc(app)
137+
138+
cy.get("@additionalRedirect").should("not.exist")
139+
140+
cy.location("pathname").should((loc) => {
141+
expect(loc).to.be.oneOf([
142+
"/welcome",
143+
"/",
144+
"/sessions",
145+
"/verification",
146+
])
98147
})
99148

100149
cy.getSession().should((session) => {

test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,26 @@ context("Social Sign In Settings Success", () => {
4040
cy.get("#accept").click()
4141

4242
cy.get('input[name="traits.website"]').clear().type(website)
43+
44+
cy.intercept({
45+
method: "POST",
46+
url: "http://localhost:4433/self-service/registration*",
47+
query: { flow: "*" },
48+
}).as("registrationCall")
4349
cy.triggerOidc(app, "hydra")
4450

45-
cy.get('[data-testid="ui/message/1010016"]').should(
46-
"contain.text",
47-
"as another way to sign in.",
48-
)
51+
if (app === "react") {
52+
cy.wait("@registrationCall").should((intercept) => {
53+
expect(intercept.response.body.ui.messages[0].text).contain(
54+
"as another way to sign in.",
55+
)
56+
})
57+
} else {
58+
cy.get('[data-testid="ui/message/1010016"]').should(
59+
"contain.text",
60+
"as another way to sign in.",
61+
)
62+
}
4963

5064
cy.noSession()
5165
}

0 commit comments

Comments
 (0)