fix: migrate external id of federated users (#6312)

* feat: migrate external id

* implement tests and some renaming

* fix projection

* cleanup

* i18n

* fix event type

* handle migration for new services as well

* typo
This commit is contained in:
Livio Spring
2023-08-04 11:35:36 +02:00
committed by GitHub
parent d33a4fbb2f
commit 45262e6829
28 changed files with 611 additions and 9 deletions

View File

@@ -15,7 +15,8 @@ import (
const (
authURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize"
tokenURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
userinfoURL string = "https://graph.microsoft.com/v1.0/me"
userURL string = "https://graph.microsoft.com/v1.0/me"
userinfoEndpoint string = "https://graph.microsoft.com/oidc/userinfo"
ScopeUserRead string = "User.Read"
)
@@ -87,7 +88,7 @@ func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts
rp, err := oauth.New(
config,
name,
userinfoURL,
userURL,
func() idp.User {
return &User{isEmailVerified: provider.emailVerified}
},

View File

@@ -0,0 +1,29 @@
package azuread
import (
"net/http"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
// Session extends the [oauth.Session] to extend it with the [idp.SessionSupportsMigration] functionality
type Session struct {
*oauth.Session
}
// RetrievePreviousID implements the [idp.SessionSupportsMigration] interface by returning the `sub` from the userinfo endpoint
func (s *Session) RetrievePreviousID() (string, error) {
req, err := http.NewRequest("GET", userinfoEndpoint, nil)
if err != nil {
return "", err
}
req.Header.Set("authorization", s.Tokens.TokenType+" "+s.Tokens.AccessToken)
userinfo := new(oidc.UserInfo)
if err := httphelper.HttpRequest(s.Provider.HttpClient(), req, &userinfo); err != nil {
return "", err
}
return userinfo.Subject, nil
}

View File

@@ -247,12 +247,12 @@ func TestSession_FetchUser(t *testing.T) {
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
require.NoError(t, err)
session := &oauth.Session{
session := &Session{Session: &oauth.Session{
AuthURL: tt.fields.authURL,
Code: tt.fields.code,
Tokens: tt.fields.tokens,
Provider: provider.Provider,
}
}}
user, err := session.FetchUser(context.Background())
if tt.want.err != nil && !tt.want.err(err) {
@@ -294,3 +294,116 @@ func userinfo() *User {
UserPrincipalName: "username",
}
}
func TestSession_RetrievePreviousID(t *testing.T) {
type fields struct {
name string
clientID string
clientSecret string
redirectURI string
scopes []string
httpMock func()
tokens *oidc.Tokens[*oidc.IDTokenClaims]
}
type res struct {
id string
err bool
}
tests := []struct {
name string
fields fields
res res
}{
{
name: "invalid token",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
httpMock: func() {
gock.New("https://graph.microsoft.com").
Get("/oidc/userinfo").
Reply(401)
},
tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: oidc.BearerToken,
},
IDTokenClaims: oidc.NewIDTokenClaims(
"https://login.microsoftonline.com/consumers/oauth2/v2.0",
"sub",
[]string{"clientID"},
time.Now().Add(1*time.Hour),
time.Now().Add(-1*time.Second),
"nonce",
"",
nil,
"clientID",
0,
),
},
},
res: res{
err: true,
},
},
{
name: "success",
fields: fields{
clientID: "clientID",
clientSecret: "clientSecret",
redirectURI: "redirectURI",
httpMock: func() {
gock.New("https://graph.microsoft.com").
Get("/oidc/userinfo").
Reply(200).
JSON(`{"sub":"sub"}`)
},
tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
TokenType: oidc.BearerToken,
},
IDTokenClaims: oidc.NewIDTokenClaims(
"https://login.microsoftonline.com/consumers/oauth2/v2.0",
"sub",
[]string{"clientID"},
time.Now().Add(1*time.Hour),
time.Now().Add(-1*time.Second),
"nonce",
"",
nil,
"clientID",
0,
),
},
},
res: res{
id: "sub",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer gock.Off()
tt.fields.httpMock()
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
require.NoError(t, err)
session := &Session{Session: &oauth.Session{
Tokens: tt.fields.tokens,
Provider: provider.Provider,
}}
id, err := session.RetrievePreviousID()
if tt.res.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
a.Equal(tt.res.id, id)
})
}
}