Skip to content

Commit 5f0ec87

Browse files
authored
feat: implement link identity with oidc / native sign in (#2108)
Implements linking an OIDC identity to a user. It works by modifying the existing `/token?grant_type=id_token` endpoint by allowing a `link_identity` body parameter. If it's set, the `Authorization` header must be set to a valid JWT representing a user. This user will be the user to which the ID token's identity will be linked to.
1 parent 609f169 commit 5f0ec87

File tree

3 files changed

+37
-11
lines changed

3 files changed

+37
-11
lines changed

internal/api/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (a *API) extractBearerToken(r *http.Request) (string, error) {
6464
authHeader := r.Header.Get("Authorization")
6565
matches := bearerRegexp.FindStringSubmatch(authHeader)
6666
if len(matches) != 2 {
67-
return "", apierrors.NewHTTPError(http.StatusUnauthorized, apierrors.ErrorCodeNoAuthorization, "This endpoint requires a Bearer token")
67+
return "", apierrors.NewHTTPError(http.StatusUnauthorized, apierrors.ErrorCodeNoAuthorization, "This endpoint requires a valid Bearer token")
6868
}
6969

7070
return matches[1], nil

internal/api/token_oidc.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ import (
1919

2020
// IdTokenGrantParams are the parameters the IdTokenGrant method accepts
2121
type IdTokenGrantParams struct {
22-
IdToken string `json:"id_token"`
23-
AccessToken string `json:"access_token"`
24-
Nonce string `json:"nonce"`
25-
Provider string `json:"provider"`
26-
ClientID string `json:"client_id"`
27-
Issuer string `json:"issuer"`
22+
IdToken string `json:"id_token"`
23+
AccessToken string `json:"access_token"`
24+
Nonce string `json:"nonce"`
25+
Provider string `json:"provider"`
26+
ClientID string `json:"client_id"`
27+
Issuer string `json:"issuer"`
28+
LinkIdentity bool `json:"link_identity"`
2829
}
2930

3031
func (p *IdTokenGrantParams) getProvider(ctx context.Context, config *conf.GlobalConfiguration, r *http.Request) (*oidc.Provider, bool, string, []string, bool, error) {
@@ -167,6 +168,25 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
167168
return apierrors.NewOAuthError("invalid request", "provider or client_id and issuer required")
168169
}
169170

171+
if params.LinkIdentity {
172+
if r.Header.Get("Authorization") == "" {
173+
return apierrors.NewOAuthError("invalid request", "Linking requires a valid user access token in Authorization")
174+
}
175+
176+
requireAuthCtx, err := a.requireAuthentication(w, r)
177+
if err != nil {
178+
return err
179+
}
180+
181+
targetUser := getUser(requireAuthCtx)
182+
if targetUser == nil {
183+
return apierrors.NewOAuthError("invalid request", "Linking requires a valid user authentication")
184+
}
185+
186+
// set it so linkIdentityToUser works below
187+
ctx = withTargetUser(ctx, targetUser)
188+
}
189+
170190
oidcProvider, skipNonceCheck, providerType, acceptableClientIDs, emailOptional, err := params.getProvider(ctx, config, r)
171191
if err != nil {
172192
return err
@@ -251,15 +271,21 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R
251271

252272
grantParams.FillGrantParams(r)
253273

254-
if err := a.triggerBeforeUserCreatedExternal(r, db, userData, providerType); err != nil {
255-
return err
274+
if !params.LinkIdentity {
275+
if err := a.triggerBeforeUserCreatedExternal(r, db, userData, providerType); err != nil {
276+
return err
277+
}
256278
}
257279

258280
if err := db.Transaction(func(tx *storage.Connection) error {
259281
var user *models.User
260282
var terr error
261283

262-
user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional)
284+
if params.LinkIdentity {
285+
user, terr = a.linkIdentityToUser(r, ctx, tx, userData, providerType)
286+
} else {
287+
user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional)
288+
}
263289
if terr != nil {
264290
return terr
265291
}

internal/e2e/e2eapi/e2eapi_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func TestDo(t *testing.T) {
139139
res := make(chan string)
140140
err := Do(ctx, http.MethodGet, inst.APIServer.URL+"/user", nil, &res)
141141
require.Error(t, err)
142-
require.ErrorContains(t, err, "401: This endpoint requires a Bearer token")
142+
require.ErrorContains(t, err, "401: This endpoint requires a valid Bearer token")
143143
})
144144

145145
// Covers http.NewRequestWithContext

0 commit comments

Comments
 (0)