Skip to content

microsoft: support client_credentials flow using client assertions #464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions microsoft/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package microsoft provides support for making OAuth2 authorized and authenticated
// HTTP requests to Microsoft APIs. It supports the client credentials flow using
// client certificates to sign a JWT assertion. For the client credentials flow using
// a shared secret, use the clientcredentials package.
//
// For more information on the client credentials flow using certificates, see
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
//
// Usage
//
// To generate a client assertion, both the private key and certificate are required. The token is signed
// using the key, but the service requires the SHA-1 hash of the certificate in order to identify the key
// being used.
//
// Scopes requested should be in the form https://api.endpoint/.default, for example
// https://graph.microsoft.com/.default
//
// The token URL for an Azure Active Directory tenant can be obtained with the AzureADEndpoint function.
//
package microsoft // import "golang.org/x/oauth2/microsoft"
26 changes: 26 additions & 0 deletions microsoft/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package microsoft_test

import (
"context"

"golang.org/x/oauth2/microsoft"
)

func ExampleClientCertificate() {
ctx := context.Background()

conf := microsoft.Config{
ClientID: "YOUR_CLIENT_ID",
PrivateKey: []byte("YOUR_ENCODED_PRIVATE_KEY"),
Certificate: []byte("YOUR_ENCODED_CERTIFICATE"),
Scopes: []string{"https://graph.microsoft.com/.default"},
TokenURL: microsoft.AzureADEndpoint("YOUR_TENANT_ID").TokenURL,
}

client := conf.Client(ctx)
client.Get("https://graph.microsoft.com/v1.0/me")
}
180 changes: 174 additions & 6 deletions microsoft/microsoft.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@
package microsoft // import "golang.org/x/oauth2/microsoft"

import (
"context"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

"golang.org/x/oauth2"
"golang.org/x/oauth2/internal"
"golang.org/x/oauth2/jws"
)

// LiveConnectEndpoint is Windows's Live ID OAuth 2.0 endpoint.
var LiveConnectEndpoint = oauth2.Endpoint{
AuthURL: "https://login.live.com/oauth20_authorize.srf",
TokenURL: "https://login.live.com/oauth20_token.srf",
}

// AzureADEndpoint returns a new oauth2.Endpoint for the given tenant at Azure Active Directory.
// If tenant is empty, it uses the tenant called `common`.
//
Expand All @@ -29,3 +39,161 @@ func AzureADEndpoint(tenant string) oauth2.Endpoint {
TokenURL: "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token",
}
}

// Config is the configuration for using client credentials flow with a client assertion.
//
// For more information see:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
type Config struct {
// ClientID is the application's ID.
ClientID string

// PrivateKey contains the contents of an RSA private key or the
// contents of a PEM file that contains a private key. The provided
// private key is used to sign JWT assertions.
// PEM containers with a passphrase are not supported.
// You can use pkcs12.Decode to extract the private key and certificate
// from a PKCS #12 archive, or alternatively with OpenSSL:
//
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
//
PrivateKey []byte

// Certificate contains the (optionally PEM encoded) X509 certificate registered
// for the application with which you are authenticating.
Certificate []byte

// Scopes optionally specifies a list of requested permission scopes.
Scopes []string

// TokenURL is the token endpoint. Typically you can use the AzureADEndpoint
// function to obtain this value, but it may change for non-public clouds.
TokenURL string

// Expires optionally specifies how long the token is valid for.
Expires time.Duration

// Audience optionally specifies the intended audience of the
// request. If empty, the value of TokenURL is used as the
// intended audience.
Audience string
}

// TokenSource returns a TokenSource using the configuration
// in c and the HTTP client from the provided context.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
return oauth2.ReuseTokenSource(nil, assertionSource{ctx, c})
}

// Client returns an HTTP client wrapping the context's
// HTTP transport and adding Authorization headers with tokens
// obtained from c.
//
// The returned client and its Transport should not be modified.
func (c *Config) Client(ctx context.Context) *http.Client {
return oauth2.NewClient(ctx, c.TokenSource(ctx))
}

// assertionSource is a source that always does a signed request for a token.
// It should typically be wrapped with a reuseTokenSource.
type assertionSource struct {
ctx context.Context
conf *Config
}

// Token refreshes the token by using a new client credentials request with signed assertion.
func (a assertionSource) Token() (*oauth2.Token, error) {
crt := a.conf.Certificate
if der, _ := pem.Decode(a.conf.Certificate); der != nil {
crt = der.Bytes
}
cert, err := x509.ParseCertificate(crt)
if err != nil {
return nil, fmt.Errorf("oauth2: cannot parse certificate: %v", err)
}
s := sha1.Sum(cert.Raw)
fp := base64.URLEncoding.EncodeToString(s[:])
h := jws.Header{
Algorithm: "RS256",
Typ: "JWT",
KeyID: fp,
}

claimSet := &jws.ClaimSet{
Iss: a.conf.ClientID,
Sub: a.conf.ClientID,
Aud: a.conf.TokenURL,
}
if t := a.conf.Expires; t > 0 {
claimSet.Exp = time.Now().Add(t).Unix()
}
if aud := a.conf.Audience; aud != "" {
claimSet.Aud = aud
}

pk, err := internal.ParseKey(a.conf.PrivateKey)
if err != nil {
return nil, err
}

payload, err := jws.Encode(&h, claimSet, pk)
if err != nil {
return nil, err
}

hc := oauth2.NewClient(a.ctx, nil)
v := url.Values{
"client_assertion": {payload},
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
"client_id": {a.conf.ClientID},
"grant_type": {"client_credentials"},
"scope": {strings.Join(a.conf.Scopes, " ")},
}
resp, err := hc.PostForm(a.conf.TokenURL, v)
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}

defer resp.Body.Close()
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}

if c := resp.StatusCode; c < 200 || c > 299 {
return nil, &oauth2.RetrieveError{
Response: resp,
Body: body,
}
}

var tokenRes struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
IDToken string `json:"id_token"`
Scope string `json:"scope"`
ExpiresIn int64 `json:"expires_in"` // relative seconds from now
ExpiresOn int64 `json:"expires_on"` // timestamp
}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}

token := &oauth2.Token{
AccessToken: tokenRes.AccessToken,
TokenType: tokenRes.TokenType,
}
if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
}
if v := tokenRes.IDToken; v != "" {
// decode returned id token to get expiry
claimSet, err := jws.Decode(v)
if err != nil {
return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
}
token.Expiry = time.Unix(claimSet.Exp, 0)
}

return token, nil
}
9 changes: 9 additions & 0 deletions microsoft/windows_live_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package microsoft

import "golang.org/x/oauth2"

// LiveConnectEndpoint is Windows's Live ID OAuth 2.0 endpoint.
var LiveConnectEndpoint = oauth2.Endpoint{
AuthURL: "https://login.live.com/oauth20_authorize.srf",
TokenURL: "https://login.live.com/oauth20_token.srf",
}