diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go index d4ce2e67..142a7c6c 100644 --- a/connector/microsoft/microsoft.go +++ b/connector/microsoft/microsoft.go @@ -31,7 +31,6 @@ const ( ) const ( - apiURL = "https://graph.microsoft.com" // 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 @@ -54,6 +53,8 @@ type Config struct { // Open returns a strategy for logging in through Microsoft. func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { m := microsoftConnector{ + apiURL: "https://login.microsoftonline.com", + graphURL: "https://graph.microsoft.com", redirectURI: c.RedirectURI, clientID: c.ClientID, clientSecret: c.ClientSecret, @@ -94,6 +95,8 @@ var ( ) type microsoftConnector struct { + apiURL string + graphURL string redirectURI string clientID string clientSecret string @@ -123,8 +126,8 @@ func (c *microsoftConnector) oauth2Config(scopes connector.Scopes) *oauth2.Confi ClientID: c.clientID, ClientSecret: c.clientSecret, Endpoint: oauth2.Endpoint{ - AuthURL: "https://login.microsoftonline.com/" + c.tenant + "/oauth2/v2.0/authorize", - TokenURL: "https://login.microsoftonline.com/" + c.tenant + "/oauth2/v2.0/token", + AuthURL: c.apiURL + "/" + c.tenant + "/oauth2/v2.0/authorize", + TokenURL: c.apiURL + "/" + c.tenant + "/oauth2/v2.0/token", }, Scopes: microsoftScopes, RedirectURL: c.redirectURI, @@ -296,7 +299,7 @@ type user struct { 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", apiURL+"/v1.0/me?$select=id,displayName,userPrincipalName", nil) + 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) } @@ -355,7 +358,7 @@ func (c *microsoftConnector) getGroupIDs(ctx context.Context, client *http.Clien in := &struct { SecurityEnabledOnly bool `json:"securityEnabledOnly"` }{c.onlySecurityGroups} - reqURL := apiURL + "/v1.0/me/getMemberGroups" + reqURL := c.graphURL + "/v1.0/me/getMemberGroups" for { var out []string var next string @@ -383,7 +386,7 @@ func (c *microsoftConnector) getGroupNames(ctx context.Context, client *http.Cli IDs []string `json:"ids"` Types []string `json:"types"` }{ids, []string{"group"}} - reqURL := apiURL + "/v1.0/directoryObjects/getByIds" + reqURL := c.graphURL + "/v1.0/directoryObjects/getByIds" for { var out []group var next string diff --git a/connector/microsoft/microsoft_test.go b/connector/microsoft/microsoft_test.go new file mode 100644 index 00000000..3fba9c2a --- /dev/null +++ b/connector/microsoft/microsoft_test.go @@ -0,0 +1,90 @@ +package microsoft + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "reflect" + "testing" + + "github.com/dexidp/dex/connector" +) + +type testResponse struct { + data interface{} +} + +const tenant = "9b1c3439-a67e-4e92-bb0d-0571d44ca965" + +var dummyToken = testResponse{data: map[string]interface{}{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", + "expires_in": "30", +}} + +func TestUserIdentityFromGraphAPI(t *testing.T) { + s := newTestServer(map[string]testResponse{ + "/v1.0/me?$select=id,displayName,userPrincipalName": { + data: user{ID: "S56767889", Name: "Jane Doe", Email: "jane.doe@example.com"}, + }, + "/" + tenant + "/oauth2/v2.0/token": dummyToken, + }) + defer s.Close() + + req, _ := http.NewRequest("GET", s.URL, nil) + + c := microsoftConnector{apiURL: s.URL, graphURL: s.URL, tenant: tenant} + identity, err := c.HandleCallback(connector.Scopes{Groups: false}, req) + expectNil(t, err) + expectEquals(t, identity.Username, "Jane Doe") + expectEquals(t, identity.UserID, "S56767889") + expectEquals(t, identity.PreferredUsername, "") + expectEquals(t, identity.Email, "jane.doe@example.com") + expectEquals(t, identity.EmailVerified, true) + expectEquals(t, len(identity.Groups), 0) +} + +func TestUserGroupsFromGraphAPI(t *testing.T) { + s := newTestServer(map[string]testResponse{ + "/v1.0/me?$select=id,displayName,userPrincipalName": {data: user{}}, + "/v1.0/me/getMemberGroups": {data: map[string]interface{}{ + "value": []string{"a", "b"}, + }}, + "/" + tenant + "/oauth2/v2.0/token": dummyToken, + }) + defer s.Close() + + req, _ := http.NewRequest("GET", s.URL, nil) + + c := microsoftConnector{apiURL: s.URL, graphURL: s.URL, tenant: tenant} + identity, err := c.HandleCallback(connector.Scopes{Groups: true}, req) + expectNil(t, err) + expectEquals(t, identity.Groups, []string{"a", "b"}) +} + +func newTestServer(responses map[string]testResponse) *httptest.Server { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response, found := responses[r.RequestURI] + if !found { + fmt.Fprintf(os.Stderr, "Mock response for %q not found\n", r.RequestURI) + http.NotFound(w, r) + return + } + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(response.data) + })) + return s +} + +func expectNil(t *testing.T, a interface{}) { + if a != nil { + t.Errorf("Expected %+v to equal nil", a) + } +} + +func expectEquals(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Errorf("Expected %+v to equal %+v", a, b) + } +}