Skip to content

Commit

Permalink
feat: extend Microsoft Graph API capabilities, with fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
alnr committed Feb 12, 2024
1 parent e033fc0 commit 05734ed
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 25 deletions.
80 changes: 56 additions & 24 deletions selfservice/strategy/oidc/provider_microsoft.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strings"

"github.com/hashicorp/go-retryablehttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"

"github.com/ory/x/httpx"

Expand All @@ -18,6 +20,7 @@ import (

gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"golang.org/x/oauth2"

"github.com/ory/herodot"
Expand Down Expand Up @@ -85,39 +88,68 @@ func (m *ProviderMicrosoft) Claims(ctx context.Context, exchange *oauth2.Token,
}

func (m *ProviderMicrosoft) updateSubject(ctx context.Context, claims *Claims, exchange *oauth2.Token) (*Claims, error) {
if m.config.SubjectSource == "me" {
o, err := m.OAuth2(ctx)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
}
if m.config.SubjectSource != "me" {
return claims, nil

Check warning on line 92 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L92

Added line #L92 was not covered by tests
}

ctx, client := httpx.SetOAuth2(ctx, m.reg.HTTPClient(ctx), o, exchange)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://graph.microsoft.com/v1.0/me", nil)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
}
o, err := m.OAuth2(ctx)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))

Check warning on line 97 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L97

Added line #L97 was not covered by tests
}

resp, err := client.Do(req)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to fetch from `https://graph.microsoft.com/v1.0/me`: %s", err))
}
defer resp.Body.Close()
if claims.RawClaims == nil {
claims.RawClaims = map[string]interface{}{}

Check warning on line 101 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L101

Added line #L101 was not covered by tests
}

if err := logUpstreamError(m.reg.Logger(), resp); err != nil {
ctx, client := httpx.SetOAuth2(ctx, m.reg.HTTPClient(ctx), o, exchange)

// Query to request most user fields from the Graph API (User.Read scope)
// https://learn.microsoft.com/en-us/previous-versions/azure/ad/graph/api/entity-and-complex-type-reference#user-entity
// Nota bene: some fields are returned from the API when no $select is specified which are not listed in the above link!
// The query below was constructed by using the Graph Explorer and comparing against a query without $select.
// https://developer.microsoft.com/en-us/graph/graph-explorer?request=me%3F%24select%3Did%2CbusinessPhones%2CaccountEnabled%2Ccity%2Ccountry%2CcreationType%2CdeletionTimestamp%2Cdepartment%2CdirSyncEnabled%2CdisplayName%2CemployeeId%2CfacsimileTelephoneNumber%2CgivenName%2CimmutableId%2CjobTitle%2ClastDirSyncTime%2Cmail%2CmobilePhone%2CofficeLocation%2CmailNickname%2Cmobile%2CobjectId%2CobjectType%2ConPremisesSecurityIdentifier%2CotherMails%2CpasswordPolicies%2CpasswordProfile%2CphysicalDeliveryOfficeName%2CpostalCode%2CpreferredLanguage%2CproxyAddresses%2CrefreshTokensValidFromDateTime%2CshowInAddressList%2CsignInNames%2CsipProxyAddress%2Cstate%2CstreetAddress%2Csurname%2CtelephoneNumber%2CthumbnailPhoto%2CusageLocation%2CuserIdentities%2CuserPrincipalName%2CuserType&method=GET&version=v1.0&GraphUrl=https://graph.microsoft.com
query := "?$select=id,businessPhones,accountEnabled,city,country,creationType,deletionTimestamp,department,dirSyncEnabled,displayName,employeeId,facsimileTelephoneNumber,givenName,immutableId,jobTitle,lastDirSyncTime,mail,mobilePhone,officeLocation,mailNickname,mobile,objectId,objectType,onPremisesSecurityIdentifier,otherMails,passwordPolicies,passwordProfile,physicalDeliveryOfficeName,postalCode,preferredLanguage,proxyAddresses,refreshTokensValidFromDateTime,showInAddressList,signInNames,sipProxyAddress,state,streetAddress,surname,telephoneNumber,thumbnailPhoto,usageLocation,userIdentities,userPrincipalName,userType"
claims.Subject, claims.RawClaims["user"], err = m.subjectFromGraphAPI(ctx, client, query)
if err != nil {
m.reg.Logger().WithError(err).Error("Unable to fetch user from Microsoft Graph API with $select. Attempting fallback without $select.")
// fallback code path when the MS GraphAPI does not respond as expected
query = ""
claims.Subject, claims.RawClaims["user"], err = m.subjectFromGraphAPI(ctx, client, query)
if err != nil {
return nil, err
}
}

var user struct {
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode JSON from `https://graph.microsoft.com/v1.0/me`: %s", err))
}
return claims, nil
}

claims.Subject = user.ID
func (m *ProviderMicrosoft) subjectFromGraphAPI(ctx context.Context, client *retryablehttp.Client, query string) (subject string, rawClaims map[string]any, err error) {
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", "https://graph.microsoft.com/v1.0/me"+query, nil)
if err != nil {
return "", nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))

Check warning on line 129 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L129

Added line #L129 was not covered by tests
}

return claims, nil
resp, err := client.Do(req)
if err != nil {
return "", nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to fetch from `https://graph.microsoft.com/v1.0/me`: %s", err))

Check warning on line 134 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L134

Added line #L134 was not covered by tests
}
defer resp.Body.Close()

if err := logUpstreamError(m.reg.Logger(), resp); err != nil {
return "", nil, err
}

if err := json.NewDecoder(resp.Body).Decode(&rawClaims); err != nil {
return "", nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode JSON from `https://graph.microsoft.com/v1.0/me`: %s", err))

Check warning on line 143 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L143

Added line #L143 was not covered by tests
}

trace.SpanFromContext(ctx).SetAttributes(attribute.StringSlice("microsoft.user.claims", maps.Keys(rawClaims)))

ok := false
if subject, ok = rawClaims["id"].(string); !ok {
return "", nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to retrieve subject from `https://graph.microsoft.com/v1.0/me`: %s", err))

Check warning on line 150 in selfservice/strategy/oidc/provider_microsoft.go

View check run for this annotation

Codecov / codecov/patch

selfservice/strategy/oidc/provider_microsoft.go#L150

Added line #L150 was not covered by tests
}
return subject, rawClaims, nil
}

type microsoftUnverifiedClaims struct {
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/oidc/provider_userinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) {
},
expectedClaims: &oidc.Claims{
Issuer: "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", Subject: "new-id", Name: "John Doe", Email: "[email protected]",
RawClaims: map[string]interface{}{"aud": []interface{}{"foo"}, "exp": 4.071728504e+09, "iat": 1.516239022e+09, "iss": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", "email": "[email protected]", "name": "John Doe", "sub": "1234567890", "tid": "a9b86385-f32c-4803-afc8-4b2312fbdf24"},
RawClaims: map[string]interface{}{"aud": []interface{}{"foo"}, "exp": 4.071728504e+09, "iat": 1.516239022e+09, "iss": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", "email": "[email protected]", "name": "John Doe", "sub": "1234567890", "tid": "a9b86385-f32c-4803-afc8-4b2312fbdf24", "user": map[string]interface{}{"id": "new-id"}},
},
},
{
Expand Down

0 comments on commit 05734ed

Please sign in to comment.