Skip to content

Commit

Permalink
Merge pull request #20465 from hashicorp/bugfix/oid-missing-claim
Browse files Browse the repository at this point in the history
Bug fix: support missing `oid` claim
  • Loading branch information
tombuildsstuff authored Feb 16, 2023
2 parents d5f4a07 + 163c437 commit 2bbf5c9
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 29 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.1.2
github.com/hashicorp/go-azure-helpers v0.51.0
github.com/hashicorp/go-azure-sdk v0.20230215.1153645
github.com/hashicorp/go-azure-sdk v0.20230216.1112535
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-azure-helpers v0.12.0/go.mod h1:Zc3v4DNeX6PDdy7NljlYpnrdac1++qNW0I4U+ofGwpg=
github.com/hashicorp/go-azure-helpers v0.51.0 h1:8KSDGkGnWH6zOT60R3KUqsi0fk1vA7AMunaOUJZMM6k=
github.com/hashicorp/go-azure-helpers v0.51.0/go.mod h1:lsykLR4KjTUO7MiRmNWiTiX8QQtw3ILjyOvT0f5h3rw=
github.com/hashicorp/go-azure-sdk v0.20230215.1153645 h1:Sh5Zw8xBPKnD4A4X0ZQ00B9zk+grCsSvzLo6WJfB0kc=
github.com/hashicorp/go-azure-sdk v0.20230215.1153645/go.mod h1:aHinadEuBi04I1i+yvpPMZUxvxRxl5JgBOwlzIIxozU=
github.com/hashicorp/go-azure-sdk v0.20230216.1112535 h1:hlMwbjtj27LPUvITk9yZ3sJ3+wFalFIz6FQ0XoQQCD8=
github.com/hashicorp/go-azure-sdk v0.20230216.1112535/go.mod h1:aHinadEuBi04I1i+yvpPMZUxvxRxl5JgBOwlzIIxozU=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
Expand Down
50 changes: 45 additions & 5 deletions internal/clients/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package clients
import (
"context"
"fmt"
"log"
"net/http"
"strings"

"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/go-azure-sdk/sdk/auth"
"github.com/hashicorp/go-azure-sdk/sdk/claims"
"github.com/hashicorp/go-azure-sdk/sdk/environments"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients/graph"
)

type ResourceManagerAccount struct {
Expand All @@ -27,7 +29,12 @@ type ResourceManagerAccount struct {
AzureEnvironment azure.Environment
}

func NewResourceManagerAccount(ctx context.Context, authorizer auth.Authorizer, config auth.Credentials, subscriptionId string, skipResourceProviderRegistration bool, azureEnvironment azure.Environment) (*ResourceManagerAccount, error) {
func NewResourceManagerAccount(ctx context.Context, config auth.Credentials, subscriptionId string, skipResourceProviderRegistration bool, azureEnvironment azure.Environment) (*ResourceManagerAccount, error) {
authorizer, err := auth.NewAuthorizerFromCredentials(ctx, config, config.Environment.MicrosoftGraph)
if err != nil {
return nil, fmt.Errorf("unable to build authorizer for Microsoft Graph API: %+v", err)
}

// Acquire an access token so we can inspect the claims
token, err := authorizer.Token(ctx, &http.Request{})
if err != nil {
Expand All @@ -40,17 +47,50 @@ func NewResourceManagerAccount(ctx context.Context, authorizer auth.Authorizer,
}

authenticatedAsServicePrincipal := true
if strings.Contains(strings.ToLower(claims.Scopes), "user_impersonation") {
if strings.Contains(strings.ToLower(claims.Scopes), "openid") {
authenticatedAsServicePrincipal = false
}

clientId := claims.AppId
if clientId == "" {
log.Printf("[DEBUG] Using user-supplied ClientID because the `appid` claim was missing from the access token")
clientId = config.ClientID
}

objectId := claims.ObjectId
if objectId == "" {
if authenticatedAsServicePrincipal {
log.Printf("[DEBUG] Querying Microsoft Graph to discover authenticated service principal object ID because the `oid` claim was missing from the access token")
id, err := graph.ServicePrincipalObjectID(ctx, authorizer, config.Environment, config.ClientID)
if err != nil {
return nil, fmt.Errorf("attempting to discover object ID for authenticated service principal with client ID %q: %+v", config.ClientID, err)
}

objectId = *id
} else {
log.Printf("[DEBUG] Querying Microsoft Graph to discover authenticated user principal object ID because the `oid` claim was missing from the access token")
id, err := graph.UserPrincipalObjectID(ctx, authorizer, config.Environment)
if err != nil {
return nil, fmt.Errorf("attempting to discover object ID for authenticated user principal: %+v", err)
}

objectId = *id
}
}

tenantId := claims.TenantId
if tenantId == "" {
log.Printf("[DEBUG] Using user-supplied TenantID because the `tid` claim was missing from the access token")
tenantId = config.TenantID
}

account := ResourceManagerAccount{
Environment: config.Environment,

ClientId: claims.AppId,
ObjectId: claims.ObjectId,
ClientId: clientId,
ObjectId: objectId,
SubscriptionId: subscriptionId,
TenantId: claims.TenantId,
TenantId: tenantId,

AuthenticatedAsAServicePrincipal: authenticatedAsServicePrincipal,
SkipResourceProviderRegistration: skipResourceProviderRegistration,
Expand Down
2 changes: 1 addition & 1 deletion internal/clients/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func Build(ctx context.Context, builder ClientBuilder) (*Client, error) {
}
resourceManagerEndpoint, _ := builder.AuthConfig.Environment.ResourceManager.Endpoint()

account, err := NewResourceManagerAccount(ctx, resourceManagerAuth, *builder.AuthConfig, builder.SubscriptionID, builder.SkipProviderRegistration, *azureEnvironment)
account, err := NewResourceManagerAccount(ctx, *builder.AuthConfig, builder.SubscriptionID, builder.SkipProviderRegistration, *azureEnvironment)
if err != nil {
return nil, fmt.Errorf("building account: %+v", err)
}
Expand Down
148 changes: 148 additions & 0 deletions internal/clients/graph/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package graph

import (
"context"
"fmt"
"net/http"
"time"

"github.com/hashicorp/go-azure-sdk/sdk/auth"
"github.com/hashicorp/go-azure-sdk/sdk/client"
"github.com/hashicorp/go-azure-sdk/sdk/client/msgraph"
"github.com/hashicorp/go-azure-sdk/sdk/environments"
"github.com/hashicorp/go-azure-sdk/sdk/odata"
)

type options struct {
query odata.Query
}

func (o options) ToHeaders() *client.Headers {
h := client.Headers{}
h.AppendHeader(o.query.Headers())
return &h
}

func (o options) ToOData() *odata.Query {
return &o.query
}

func (o options) ToQuery() *client.QueryParams {
q := client.QueryParams{}
q.AppendValues(o.query.Values())
return &q
}

type directoryObjectModel struct {
ID *string `json:"id"`
}

func graphClient(authorizer auth.Authorizer, environment environments.Environment) (*msgraph.Client, error) {
client, err := msgraph.NewMsGraphClient(environment.MicrosoftGraph, msgraph.VersionOnePointZero)
if err != nil {
return nil, fmt.Errorf("building client: %+v", err)
}

client.Authorizer = authorizer

return client, nil
}

func ServicePrincipalObjectID(ctx context.Context, authorizer auth.Authorizer, environment environments.Environment, clientId string) (*string, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
}

opts := client.RequestOptions{
ContentType: "application/json",
ExpectedStatusCodes: []int{
http.StatusOK,
},
HttpMethod: http.MethodGet,
OptionsObject: options{
query: odata.Query{
Filter: fmt.Sprintf("appId eq '%s'", clientId),
},
},
Path: "/servicePrincipals",
}

client, err := graphClient(authorizer, environment)
if err != nil {
return nil, err
}

req, err := client.NewRequest(ctx, opts)
if err != nil {
return nil, fmt.Errorf("building new request: %+v", err)
}

resp, err := req.Execute(ctx)
if err != nil {
return nil, fmt.Errorf("executing request: %+v", err)
}

model := struct {
ServicePrincipals []directoryObjectModel `json:"value"`
}{}
if err := resp.Unmarshal(&model); err != nil {
return nil, fmt.Errorf("unmarshaling response: %+v", err)
}

if len(model.ServicePrincipals) != 1 {
return nil, fmt.Errorf("unexpected number of results, expected 1, received %d", len(model.ServicePrincipals))
}

id := model.ServicePrincipals[0].ID
if id == nil {
return nil, fmt.Errorf("returned object ID was nil")
}

return id, nil
}

func UserPrincipalObjectID(ctx context.Context, authorizer auth.Authorizer, environment environments.Environment) (*string, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Minute))
defer cancel()
}

opts := client.RequestOptions{
ContentType: "application/json",
ExpectedStatusCodes: []int{
http.StatusOK,
},
HttpMethod: http.MethodGet,
OptionsObject: nil,
Path: "/me",
}

client, err := graphClient(authorizer, environment)
if err != nil {
return nil, err
}

req, err := client.NewRequest(ctx, opts)
if err != nil {
return nil, fmt.Errorf("building new request: %+v", err)
}

resp, err := req.Execute(ctx)
if err != nil {
return nil, fmt.Errorf("executing request: %+v", err)
}

model := directoryObjectModel{}
if err := resp.Unmarshal(&model); err != nil {
return nil, fmt.Errorf("unmarshaling response: %+v", err)
}

if model.ID == nil {
return nil, fmt.Errorf("returned object ID was nil")
}

return model.ID, nil
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2bbf5c9

Please sign in to comment.