mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 05:07:31 +00:00
feat: add azure provider templates (#5441)
Adds possibility to manage and use Microsoft Azure template based providers
This commit is contained in:
@@ -15,7 +15,7 @@ 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/oidc/userinfo"
|
||||
userinfoURL string = "https://graph.microsoft.com/v1.0/me"
|
||||
)
|
||||
|
||||
// TenantType are the well known tenant types to scope the users that can authenticate. TenantType is not an
|
||||
@@ -73,7 +73,7 @@ func WithOAuthOptions(opts ...oauth.ProviderOpts) ProviderOptions {
|
||||
|
||||
// New creates an AzureAD provider using the [oauth.Provider] (OAuth 2.0 generic provider).
|
||||
// By default, it uses the [CommonTenant] and unverified emails.
|
||||
func New(name, clientID, clientSecret, redirectURI string, opts ...ProviderOptions) (*Provider, error) {
|
||||
func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts ...ProviderOptions) (*Provider, error) {
|
||||
provider := &Provider{
|
||||
tenant: CommonTenant,
|
||||
options: make([]oauth.ProviderOpts, 0),
|
||||
@@ -81,7 +81,7 @@ func New(name, clientID, clientSecret, redirectURI string, opts ...ProviderOptio
|
||||
for _, opt := range opts {
|
||||
opt(provider)
|
||||
}
|
||||
config := newConfig(provider.tenant, clientID, clientSecret, redirectURI, []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail})
|
||||
config := newConfig(provider.tenant, clientID, clientSecret, redirectURI, scopes)
|
||||
rp, err := oauth.New(
|
||||
config,
|
||||
name,
|
||||
@@ -121,34 +121,38 @@ func newConfig(tenant TenantType, clientID, secret, callbackURL string, scopes [
|
||||
// AzureAD does not return an `email_verified` claim.
|
||||
// The verification can be automatically activated on the provider ([WithEmailVerified])
|
||||
type User struct {
|
||||
Sub string `json:"sub"`
|
||||
FamilyName string `json:"family_name"`
|
||||
GivenName string `json:"given_name"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email domain.EmailAddress `json:"email"`
|
||||
Picture string `json:"picture"`
|
||||
ID string `json:"id"`
|
||||
BusinessPhones []domain.PhoneNumber `json:"businessPhones"`
|
||||
DisplayName string `json:"displayName"`
|
||||
FirstName string `json:"givenName"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
Email domain.EmailAddress `json:"mail"`
|
||||
MobilePhone domain.PhoneNumber `json:"mobilePhone"`
|
||||
OfficeLocation string `json:"officeLocation"`
|
||||
PreferredLanguage string `json:"preferredLanguage"`
|
||||
LastName string `json:"surname"`
|
||||
UserPrincipalName string `json:"userPrincipalName"`
|
||||
isEmailVerified bool
|
||||
}
|
||||
|
||||
// GetID is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetID() string {
|
||||
return u.Sub
|
||||
return u.ID
|
||||
}
|
||||
|
||||
// GetFirstName is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetFirstName() string {
|
||||
return u.GivenName
|
||||
return u.FirstName
|
||||
}
|
||||
|
||||
// GetLastName is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetLastName() string {
|
||||
return u.FamilyName
|
||||
return u.LastName
|
||||
}
|
||||
|
||||
// GetDisplayName is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetDisplayName() string {
|
||||
return u.Name
|
||||
return u.DisplayName
|
||||
}
|
||||
|
||||
// GetNickname is an implementation of the [idp.User] interface.
|
||||
@@ -159,11 +163,16 @@ func (u *User) GetNickname() string {
|
||||
|
||||
// GetPreferredUsername is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetPreferredUsername() string {
|
||||
return u.PreferredUsername
|
||||
return u.UserPrincipalName
|
||||
}
|
||||
|
||||
// GetEmail is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetEmail() domain.EmailAddress {
|
||||
if u.Email == "" {
|
||||
// if the user used a social login on Azure as well, the email will be empty
|
||||
// but is used as username
|
||||
return domain.EmailAddress(u.UserPrincipalName)
|
||||
}
|
||||
return u.Email
|
||||
}
|
||||
|
||||
@@ -188,10 +197,8 @@ func (u *User) IsPhoneVerified() bool {
|
||||
}
|
||||
|
||||
// GetPreferredLanguage is an implementation of the [idp.User] interface.
|
||||
// It returns [language.Und] because AzureAD does not provide the user's language
|
||||
func (u *User) GetPreferredLanguage() language.Tag {
|
||||
// AzureAD does not provide the user's language
|
||||
return language.Und
|
||||
return language.Make(u.PreferredLanguage)
|
||||
}
|
||||
|
||||
// GetProfile is an implementation of the [idp.User] interface.
|
||||
@@ -202,5 +209,5 @@ func (u *User) GetProfile() string {
|
||||
|
||||
// GetAvatarURL is an implementation of the [idp.User] interface.
|
||||
func (u *User) GetAvatarURL() string {
|
||||
return u.Picture
|
||||
return ""
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
openid "github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
@@ -19,6 +20,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
options []ProviderOptions
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -34,7 +36,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
redirectURI: "redirectURI",
|
||||
},
|
||||
want: &oidc.Session{
|
||||
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState",
|
||||
AuthURL: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -48,7 +50,22 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: &oidc.Session{
|
||||
AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState",
|
||||
AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom scopes",
|
||||
fields: fields{
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{openid.ScopeOpenID, openid.ScopeProfile, "user"},
|
||||
options: []ProviderOptions{
|
||||
WithTenant(ConsumersTenant),
|
||||
},
|
||||
},
|
||||
want: &oidc.Session{
|
||||
AuthURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=redirectURI&response_type=code&scope=openid+profile+user&state=testState",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -57,7 +74,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...)
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
r.NoError(err)
|
||||
|
||||
session, err := provider.BeginAuth(context.Background(), "testState")
|
||||
@@ -74,6 +91,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
options []ProviderOptions
|
||||
}
|
||||
type want struct {
|
||||
@@ -98,6 +116,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: nil,
|
||||
options: nil,
|
||||
},
|
||||
want: want{
|
||||
@@ -146,7 +165,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...)
|
||||
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)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
|
@@ -25,6 +25,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
httpMock func()
|
||||
options []ProviderOptions
|
||||
authURL string
|
||||
@@ -61,7 +62,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
redirectURI: "redirectURI",
|
||||
httpMock: func() {
|
||||
gock.New("https://graph.microsoft.com").
|
||||
Get("/oidc/userinfo").
|
||||
Get("/v1.0/me").
|
||||
Reply(200).
|
||||
JSON(userinfo())
|
||||
},
|
||||
@@ -82,7 +83,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
redirectURI: "redirectURI",
|
||||
httpMock: func() {
|
||||
gock.New("https://graph.microsoft.com").
|
||||
Get("/oidc/userinfo").
|
||||
Get("/v1.0/me").
|
||||
Reply(http.StatusInternalServerError)
|
||||
},
|
||||
authURL: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid+profile+email&state=testState",
|
||||
@@ -119,7 +120,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
redirectURI: "redirectURI",
|
||||
httpMock: func() {
|
||||
gock.New("https://graph.microsoft.com").
|
||||
Get("/oidc/userinfo").
|
||||
Get("/v1.0/me").
|
||||
Reply(200).
|
||||
JSON(userinfo())
|
||||
},
|
||||
@@ -145,16 +146,20 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
},
|
||||
want: want{
|
||||
user: &User{
|
||||
Sub: "sub",
|
||||
FamilyName: "lastname",
|
||||
GivenName: "firstname",
|
||||
Name: "firstname lastname",
|
||||
PreferredUsername: "username",
|
||||
ID: "id",
|
||||
BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"},
|
||||
DisplayName: "firstname lastname",
|
||||
FirstName: "firstname",
|
||||
JobTitle: "title",
|
||||
Email: "email",
|
||||
Picture: "picture",
|
||||
MobilePhone: "mobile",
|
||||
OfficeLocation: "office",
|
||||
PreferredLanguage: "en",
|
||||
LastName: "lastname",
|
||||
UserPrincipalName: "username",
|
||||
isEmailVerified: false,
|
||||
},
|
||||
id: "sub",
|
||||
id: "id",
|
||||
firstName: "firstname",
|
||||
lastName: "lastname",
|
||||
displayName: "firstname lastname",
|
||||
@@ -164,8 +169,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
isEmailVerified: false,
|
||||
phone: "",
|
||||
isPhoneVerified: false,
|
||||
preferredLanguage: language.Und,
|
||||
avatarURL: "picture",
|
||||
preferredLanguage: language.English,
|
||||
profile: "",
|
||||
},
|
||||
},
|
||||
@@ -180,7 +184,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
},
|
||||
httpMock: func() {
|
||||
gock.New("https://graph.microsoft.com").
|
||||
Get("/oidc/userinfo").
|
||||
Get("/v1.0/me").
|
||||
Reply(200).
|
||||
JSON(userinfo())
|
||||
},
|
||||
@@ -206,16 +210,20 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
},
|
||||
want: want{
|
||||
user: &User{
|
||||
Sub: "sub",
|
||||
FamilyName: "lastname",
|
||||
GivenName: "firstname",
|
||||
Name: "firstname lastname",
|
||||
PreferredUsername: "username",
|
||||
ID: "id",
|
||||
BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"},
|
||||
DisplayName: "firstname lastname",
|
||||
FirstName: "firstname",
|
||||
JobTitle: "title",
|
||||
Email: "email",
|
||||
Picture: "picture",
|
||||
MobilePhone: "mobile",
|
||||
OfficeLocation: "office",
|
||||
PreferredLanguage: "en",
|
||||
LastName: "lastname",
|
||||
UserPrincipalName: "username",
|
||||
isEmailVerified: true,
|
||||
},
|
||||
id: "sub",
|
||||
id: "id",
|
||||
firstName: "firstname",
|
||||
lastName: "lastname",
|
||||
displayName: "firstname lastname",
|
||||
@@ -225,8 +233,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
isEmailVerified: true,
|
||||
phone: "",
|
||||
isPhoneVerified: false,
|
||||
preferredLanguage: language.Und,
|
||||
avatarURL: "picture",
|
||||
preferredLanguage: language.English,
|
||||
profile: "",
|
||||
},
|
||||
},
|
||||
@@ -237,7 +244,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
tt.fields.httpMock()
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...)
|
||||
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{
|
||||
@@ -272,15 +279,18 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func userinfo() oidc.UserInfoSetter {
|
||||
userinfo := oidc.NewUserInfo()
|
||||
userinfo.SetSubject("sub")
|
||||
userinfo.SetName("firstname lastname")
|
||||
userinfo.SetPreferredUsername("username")
|
||||
userinfo.SetNickname("nickname")
|
||||
userinfo.SetEmail("email", false) // azure add does not send the email_verified claim
|
||||
userinfo.SetPicture("picture")
|
||||
userinfo.SetGivenName("firstname")
|
||||
userinfo.SetFamilyName("lastname")
|
||||
return userinfo
|
||||
func userinfo() *User {
|
||||
return &User{
|
||||
ID: "id",
|
||||
BusinessPhones: []domain.PhoneNumber{"phone1", "phone2"},
|
||||
DisplayName: "firstname lastname",
|
||||
FirstName: "firstname",
|
||||
JobTitle: "title",
|
||||
Email: "email",
|
||||
MobilePhone: "mobile",
|
||||
OfficeLocation: "office",
|
||||
PreferredLanguage: "en",
|
||||
LastName: "lastname",
|
||||
UserPrincipalName: "username",
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user