zitadel/internal/idp/providers/azuread/azuread.go
Livio Spring 4c482619da
fix: ensure minimal scope for azure ad (#5686)
* fix: ensure minimal scope for azure ad

* docs(idps): mention scopes which are always sent

---------

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
2023-04-17 07:02:16 +00:00

237 lines
7.5 KiB
Go

package azuread
import (
"fmt"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
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"
ScopeUserRead string = "User.Read"
)
// TenantType are the well known tenant types to scope the users that can authenticate. TenantType is not an
// exclusive list of Azure Tenants which can be used. A consumer can also use their own Tenant ID to scope
// authentication to their specific Tenant either through the Tenant ID or the friendly domain name.
//
// see also https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
type TenantType string
const (
// CommonTenant allows users with both personal Microsoft accounts and work/school accounts from Azure Active
// Directory to sign in to the application.
CommonTenant TenantType = "common"
// OrganizationsTenant allows only users with work/school accounts from Azure Active Directory to sign in to the application.
OrganizationsTenant TenantType = "organizations"
// ConsumersTenant allows only users with personal Microsoft accounts (MSA) to sign in to the application.
ConsumersTenant TenantType = "consumers"
)
var _ idp.Provider = (*Provider)(nil)
// Provider is the [idp.Provider] implementation for AzureAD (V2 Endpoints)
type Provider struct {
*oauth.Provider
tenant TenantType
emailVerified bool
options []oauth.ProviderOpts
}
type ProviderOptions func(*Provider)
// WithTenant allows to set a [TenantType] (can also be a Tenant ID)
// default is CommonTenant
func WithTenant(tenantType TenantType) ProviderOptions {
return func(p *Provider) {
p.tenant = tenantType
}
}
// WithEmailVerified allows to set every email received as verified
func WithEmailVerified() ProviderOptions {
return func(p *Provider) {
p.emailVerified = true
}
}
// WithOAuthOptions allows to specify [oauth.ProviderOpts] like [oauth.WithLinkingAllowed]
func WithOAuthOptions(opts ...oauth.ProviderOpts) ProviderOptions {
return func(p *Provider) {
p.options = append(p.options, opts...)
}
}
// 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, scopes []string, opts ...ProviderOptions) (*Provider, error) {
provider := &Provider{
tenant: CommonTenant,
options: make([]oauth.ProviderOpts, 0),
}
for _, opt := range opts {
opt(provider)
}
config := newConfig(provider.tenant, clientID, clientSecret, redirectURI, scopes)
rp, err := oauth.New(
config,
name,
userinfoURL,
func() idp.User {
return &User{isEmailVerified: provider.emailVerified}
},
provider.options...,
)
if err != nil {
return nil, err
}
provider.Provider = rp
return provider, nil
}
func newConfig(tenant TenantType, clientID, secret, callbackURL string, scopes []string) *oauth2.Config {
return &oauth2.Config{
ClientID: clientID,
ClientSecret: secret,
RedirectURL: callbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf(authURLTemplate, tenant),
TokenURL: fmt.Sprintf(tokenURLTemplate, tenant),
},
Scopes: ensureMinimalScope(scopes),
}
}
// ensureMinimalScope ensures that at least openid and `User.Read` ist set
// if none is provided it will request `openid profile email phone User.Read`
func ensureMinimalScope(scopes []string) []string {
if len(scopes) == 0 {
return []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone, ScopeUserRead}
}
var openIDSet, userReadSet bool
for _, scope := range scopes {
if scope == oidc.ScopeOpenID {
openIDSet = true
continue
}
if scope == ScopeUserRead {
userReadSet = true
continue
}
}
if !openIDSet {
scopes = append(scopes, oidc.ScopeOpenID)
}
if !userReadSet {
scopes = append(scopes, ScopeUserRead)
}
return scopes
}
// User represents the structure return on the userinfo endpoint and implements the [idp.User] interface
//
// AzureAD does not return an `email_verified` claim.
// The verification can be automatically activated on the provider ([WithEmailVerified])
type User struct {
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.ID
}
// GetFirstName is an implementation of the [idp.User] interface.
func (u *User) GetFirstName() string {
return u.FirstName
}
// GetLastName is an implementation of the [idp.User] interface.
func (u *User) GetLastName() string {
return u.LastName
}
// GetDisplayName is an implementation of the [idp.User] interface.
func (u *User) GetDisplayName() string {
return u.DisplayName
}
// GetNickname is an implementation of the [idp.User] interface.
// It returns an empty string because AzureAD does not provide the user's nickname.
func (u *User) GetNickname() string {
return ""
}
// GetPreferredUsername is an implementation of the [idp.User] interface.
func (u *User) GetPreferredUsername() string {
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
}
// IsEmailVerified is an implementation of the [idp.User] interface
// returning the value specified in the creation of the [Provider].
// Default is false because AzureAD does not return an `email_verified` claim.
// The verification can be automatically activated on the provider ([WithEmailVerified]).
func (u *User) IsEmailVerified() bool {
return u.isEmailVerified
}
// GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because AzureAD does not provide the user's phone.
func (u *User) GetPhone() domain.PhoneNumber {
return ""
}
// IsPhoneVerified is an implementation of the [idp.User] interface.
// It returns false because AzureAD does not provide the user's phone.
func (u *User) IsPhoneVerified() bool {
return false
}
// GetPreferredLanguage is an implementation of the [idp.User] interface.
func (u *User) GetPreferredLanguage() language.Tag {
return language.Make(u.PreferredLanguage)
}
// GetProfile is an implementation of the [idp.User] interface.
// It returns an empty string because AzureAD does not provide the user's profile page.
func (u *User) GetProfile() string {
return ""
}
// GetAvatarURL is an implementation of the [idp.User] interface.
func (u *User) GetAvatarURL() string {
return ""
}