6
6
package microsoft // import "golang.org/x/oauth2/microsoft"
7
7
8
8
import (
9
+ "context"
10
+ "crypto/sha1"
11
+ "crypto/x509"
12
+ "encoding/base64"
13
+ "encoding/json"
14
+ "encoding/pem"
15
+ "fmt"
16
+ "io"
17
+ "io/ioutil"
18
+ "net/http"
19
+ "net/url"
20
+ "strings"
21
+ "time"
22
+
9
23
"golang.org/x/oauth2"
24
+ "golang.org/x/oauth2/internal"
25
+ "golang.org/x/oauth2/jws"
10
26
)
11
27
12
- // LiveConnectEndpoint is Windows's Live ID OAuth 2.0 endpoint.
13
- var LiveConnectEndpoint = oauth2.Endpoint {
14
- AuthURL : "https://login.live.com/oauth20_authorize.srf" ,
15
- TokenURL : "https://login.live.com/oauth20_token.srf" ,
16
- }
17
-
18
28
// AzureADEndpoint returns a new oauth2.Endpoint for the given tenant at Azure Active Directory.
19
29
// If tenant is empty, it uses the tenant called `common`.
20
30
//
@@ -29,3 +39,161 @@ func AzureADEndpoint(tenant string) oauth2.Endpoint {
29
39
TokenURL : "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token" ,
30
40
}
31
41
}
42
+
43
+ // Config is the configuration for using client credentials flow with a client assertion.
44
+ //
45
+ // For more information see:
46
+ // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
47
+ type Config struct {
48
+ // ClientID is the application's ID.
49
+ ClientID string
50
+
51
+ // PrivateKey contains the contents of an RSA private key or the
52
+ // contents of a PEM file that contains a private key. The provided
53
+ // private key is used to sign JWT assertions.
54
+ // PEM containers with a passphrase are not supported.
55
+ // You can use pkcs12.Decode to extract the private key and certificate
56
+ // from a PKCS #12 archive, or alternatively with OpenSSL:
57
+ //
58
+ // $ openssl pkcs12 -in key.p12 -out key.pem -nodes
59
+ //
60
+ PrivateKey []byte
61
+
62
+ // Certificate contains the (optionally PEM encoded) X509 certificate registered
63
+ // for the application with which you are authenticating.
64
+ Certificate []byte
65
+
66
+ // Scopes optionally specifies a list of requested permission scopes.
67
+ Scopes []string
68
+
69
+ // TokenURL is the token endpoint. Typically you can use the AzureADEndpoint
70
+ // function to obtain this value, but it may change for non-public clouds.
71
+ TokenURL string
72
+
73
+ // Expires optionally specifies how long the token is valid for.
74
+ Expires time.Duration
75
+
76
+ // Audience optionally specifies the intended audience of the
77
+ // request. If empty, the value of TokenURL is used as the
78
+ // intended audience.
79
+ Audience string
80
+ }
81
+
82
+ // TokenSource returns a TokenSource using the configuration
83
+ // in c and the HTTP client from the provided context.
84
+ func (c * Config ) TokenSource (ctx context.Context ) oauth2.TokenSource {
85
+ return oauth2 .ReuseTokenSource (nil , assertionSource {ctx , c })
86
+ }
87
+
88
+ // Client returns an HTTP client wrapping the context's
89
+ // HTTP transport and adding Authorization headers with tokens
90
+ // obtained from c.
91
+ //
92
+ // The returned client and its Transport should not be modified.
93
+ func (c * Config ) Client (ctx context.Context ) * http.Client {
94
+ return oauth2 .NewClient (ctx , c .TokenSource (ctx ))
95
+ }
96
+
97
+ // assertionSource is a source that always does a signed request for a token.
98
+ // It should typically be wrapped with a reuseTokenSource.
99
+ type assertionSource struct {
100
+ ctx context.Context
101
+ conf * Config
102
+ }
103
+
104
+ // Token refreshes the token by using a new client credentials request with signed assertion.
105
+ func (a assertionSource ) Token () (* oauth2.Token , error ) {
106
+ crt := a .conf .Certificate
107
+ if der , _ := pem .Decode (a .conf .Certificate ); der != nil {
108
+ crt = der .Bytes
109
+ }
110
+ cert , err := x509 .ParseCertificate (crt )
111
+ if err != nil {
112
+ return nil , fmt .Errorf ("oauth2: cannot parse certificate: %v" , err )
113
+ }
114
+ s := sha1 .Sum (cert .Raw )
115
+ fp := base64 .URLEncoding .EncodeToString (s [:])
116
+ h := jws.Header {
117
+ Algorithm : "RS256" ,
118
+ Typ : "JWT" ,
119
+ KeyID : fp ,
120
+ }
121
+
122
+ claimSet := & jws.ClaimSet {
123
+ Iss : a .conf .ClientID ,
124
+ Sub : a .conf .ClientID ,
125
+ Aud : a .conf .TokenURL ,
126
+ }
127
+ if t := a .conf .Expires ; t > 0 {
128
+ claimSet .Exp = time .Now ().Add (t ).Unix ()
129
+ }
130
+ if aud := a .conf .Audience ; aud != "" {
131
+ claimSet .Aud = aud
132
+ }
133
+
134
+ pk , err := internal .ParseKey (a .conf .PrivateKey )
135
+ if err != nil {
136
+ return nil , err
137
+ }
138
+
139
+ payload , err := jws .Encode (& h , claimSet , pk )
140
+ if err != nil {
141
+ return nil , err
142
+ }
143
+
144
+ hc := oauth2 .NewClient (a .ctx , nil )
145
+ v := url.Values {
146
+ "client_assertion" : {payload },
147
+ "client_assertion_type" : {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
148
+ "client_id" : {a .conf .ClientID },
149
+ "grant_type" : {"client_credentials" },
150
+ "scope" : {strings .Join (a .conf .Scopes , " " )},
151
+ }
152
+ resp , err := hc .PostForm (a .conf .TokenURL , v )
153
+ if err != nil {
154
+ return nil , fmt .Errorf ("oauth2: cannot fetch token: %v" , err )
155
+ }
156
+
157
+ defer resp .Body .Close ()
158
+ body , err := ioutil .ReadAll (io .LimitReader (resp .Body , 1 << 20 ))
159
+ if err != nil {
160
+ return nil , fmt .Errorf ("oauth2: cannot fetch token: %v" , err )
161
+ }
162
+
163
+ if c := resp .StatusCode ; c < 200 || c > 299 {
164
+ return nil , & oauth2.RetrieveError {
165
+ Response : resp ,
166
+ Body : body ,
167
+ }
168
+ }
169
+
170
+ var tokenRes struct {
171
+ AccessToken string `json:"access_token"`
172
+ TokenType string `json:"token_type"`
173
+ IDToken string `json:"id_token"`
174
+ Scope string `json:"scope"`
175
+ ExpiresIn int64 `json:"expires_in"` // relative seconds from now
176
+ ExpiresOn int64 `json:"expires_on"` // timestamp
177
+ }
178
+ if err := json .Unmarshal (body , & tokenRes ); err != nil {
179
+ return nil , fmt .Errorf ("oauth2: cannot fetch token: %v" , err )
180
+ }
181
+
182
+ token := & oauth2.Token {
183
+ AccessToken : tokenRes .AccessToken ,
184
+ TokenType : tokenRes .TokenType ,
185
+ }
186
+ if secs := tokenRes .ExpiresIn ; secs > 0 {
187
+ token .Expiry = time .Now ().Add (time .Duration (secs ) * time .Second )
188
+ }
189
+ if v := tokenRes .IDToken ; v != "" {
190
+ // decode returned id token to get expiry
191
+ claimSet , err := jws .Decode (v )
192
+ if err != nil {
193
+ return nil , fmt .Errorf ("oauth2: error decoding JWT token: %v" , err )
194
+ }
195
+ token .Expiry = time .Unix (claimSet .Exp , 0 )
196
+ }
197
+
198
+ return token , nil
199
+ }
0 commit comments