@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/dexidp/dex/connector"
groups_pkg "github.com/dexidp/dex/pkg/groups"
"github.com/dexidp/dex/pkg/log"
@ -32,8 +33,6 @@ const (
)
const (
// Microsoft requires this scope to access user's profile
scopeUser = "user.read"
// Microsoft requires this scope to list groups the user is a member of
// and resolve their ids to groups names.
scopeGroups = "directory.read.all"
@ -59,7 +58,7 @@ type Config struct {
PromptType string ` json:"promptType" `
DomainHint string ` json:"domainHint" `
Scopes [ ] string ` json:"scopes" ` // defaults to scopeUser (user.read)
Scopes [ ] string ` json:"scopes" ` // defaults to openid,profile,email
}
// Open returns a strategy for logging in through Microsoft.
@ -86,6 +85,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
if m . tenant == "" {
m . tenant = "common"
}
m . issuer = m . apiURL + "/{tenantid}/v2.0"
// By default, use group names
switch m . groupNameFormat {
@ -96,6 +96,24 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
return nil , fmt . Errorf ( "invalid groupNameFormat: %s" , m . groupNameFormat )
}
issuer := strings . ReplaceAll ( m . issuer , "{tenantid}" , m . tenant )
ctx := oidc . InsecureIssuerURLContext ( context . Background ( ) , issuer )
provider , err := oidc . NewProvider ( ctx , issuer )
if err != nil {
return nil , fmt . Errorf ( "provider error: %v" , err )
}
var pclaims map [ string ] interface { }
if err := provider . Claims ( & pclaims ) ; err != nil {
return nil , fmt . Errorf ( "oidc: failed to decode provider claims: %v" , err )
}
if pissuer , found := pclaims [ "issuer" ] ; ! found || pissuer != m . issuer {
return nil , fmt . Errorf ( "oidc: incorrect prodiver issuer in well known configuration %q" , pissuer )
}
m . provider = provider
return & m , nil
}
@ -113,6 +131,8 @@ var (
type microsoftConnector struct {
apiURL string
graphURL string
issuer string // issuer for discovery of well known configuration
provider * oidc . Provider
redirectURI string
clientID string
clientSecret string
@ -141,7 +161,7 @@ func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Confi
if len ( c . scopes ) > 0 {
microsoftScopes = c . scopes
} else {
microsoftScopes = append ( microsoftScopes , scopeUser )
microsoftScopes = append ( microsoftScopes , oidc . ScopeOpenID , "profile" , "email" )
}
if c . groupsRequired ( scopes . Groups ) {
microsoftScopes = append ( microsoftScopes , scopeGroups )
@ -196,7 +216,7 @@ func (c *microsoftConnector) HandleCallback(s connector.Scopes, r *http.Request)
client := oauth2Config . Client ( ctx , token )
user , err := c . user ( ctx , client )
user , err := c . userFromToken ( ctx , token )
if err != nil {
return identity , fmt . Errorf ( "microsoft: get user: %v" , err )
}
@ -295,7 +315,7 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id
return nil
} ,
} )
user , err := c . user ( ctx , clien t)
user , err := c . userFromToken ( ctx , tok )
if err != nil {
return identity , fmt . Errorf ( "microsoft: get user: %v" , err )
}
@ -314,57 +334,68 @@ func (c *microsoftConnector) Refresh(ctx context.Context, s connector.Scopes, id
return identity , nil
}
// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/user
// id - The unique identifier for the user. Inherited from
//
// directoryObject. Key. Not nullable. Read-only.
//
// displayName - The name displayed in the address book for the user.
//
// This is usually the combination of the user's first name,
// middle initial and last name. This property is required
// when a user is created and it cannot be cleared during
// updates. Supports $filter and $orderby.
//
// userPrincipalName - The user principal name (UPN) of the user.
//
// The UPN is an Internet-style login name for the user
// based on the Internet standard RFC 822. By convention,
// this should map to the user's email name. The general
// format is alias@domain, where domain must be present in
// the tenant’s collection of verified domains. This
// property is required when a user is created. The
// verified domains for the tenant can be accessed from the
// verifiedDomains property of organization. Supports
// $filter and $orderby.
type user struct {
ID string ` json:"id" `
Name string ` json:"displayName" `
Email string ` json:"userPrincipalName" `
}
func ( c * microsoftConnector ) user ( ctx context . Context , client * http . Client ) ( u user , err error ) {
// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_get
req , err := http . NewRequest ( "GET" , c . graphURL + "/v1.0/me?$select=id,displayName,userPrincipalName" , nil )
if err != nil {
return u , fmt . Errorf ( "new req: %v" , err )
func ( c * microsoftConnector ) userFromToken ( ctx context . Context , token * oauth2 . Token ) ( u user , err error ) {
rawIDToken , ok := token . Extra ( "id_token" ) . ( string )
if ! ok {
return u , errors . New ( "oidc: no id_token in token response" )
}
resp , err := client . Do ( req . WithContext ( ctx ) )
// NOTE: issuer is verified below manually
verifier := c . provider . Verifier (
& oidc . Config { ClientID : c . clientID , SkipIssuerCheck : true } ,
)
idToken , err := verifier . Verify ( ctx , rawIDToken )
if err != nil {
return u , fmt . Errorf ( "get URL %v" , err )
return u , fmt . Errorf ( "oidc: failed to verify ID Token: %v" , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return u , newGraphError ( resp . Body )
var claims map [ string ] interface { }
if err := idToken . Claims ( & claims ) ; err != nil {
return u , fmt . Errorf ( "oidc: failed to decode claims: %v" , err )
}
// https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
tid , found := claims [ "tid" ] . ( string )
if ! found {
return u , errors . New ( "missing 'tid' claim" )
}
iss , found := claims [ "iss" ] . ( string )
if ! found {
return u , errors . New ( "missing 'iss' claim" )
}
objectID , found := claims [ "oid" ] . ( string )
if ! found {
return u , errors . New ( "missing 'oid' claim" )
}
name , found := claims [ "name" ] . ( string )
if ! found {
return u , errors . New ( "missing 'name' claim" )
}
email , found := claims [ "email" ] . ( string )
if ! found {
return u , errors . New ( "missing 'email' claim" )
}
if err := json . NewDecoder ( resp . Body ) . Decode ( & u ) ; err != nil {
return u , fmt . Errorf ( "JSON decode: %v" , err )
if iss != strings . ReplaceAll ( c . issuer , "{tenantid}" , tid ) {
return u , fmt . Errorf ( "incorrect token issuer:" , iss )
}
return u , err
return user {
ID : objectID ,
Name : name ,
Email : email ,
} , nil
}
// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/group