1// Copyright 2014 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly 6// known as "two-legged OAuth 2.0". 7// 8// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 9package jwt 10 11import ( 12 "context" 13 "encoding/json" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "net/http" 18 "net/url" 19 "strings" 20 "time" 21 22 "golang.org/x/oauth2" 23 "golang.org/x/oauth2/internal" 24 "golang.org/x/oauth2/jws" 25) 26 27var ( 28 defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" 29 defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} 30) 31 32// Config is the configuration for using JWT to fetch tokens, 33// commonly known as "two-legged OAuth 2.0". 34type Config struct { 35 // Email is the OAuth client identifier used when communicating with 36 // the configured OAuth provider. 37 Email string 38 39 // PrivateKey contains the contents of an RSA private key or the 40 // contents of a PEM file that contains a private key. The provided 41 // private key is used to sign JWT payloads. 42 // PEM containers with a passphrase are not supported. 43 // Use the following command to convert a PKCS 12 file into a PEM. 44 // 45 // $ openssl pkcs12 -in key.p12 -out key.pem -nodes 46 // 47 PrivateKey []byte 48 49 // PrivateKeyID contains an optional hint indicating which key is being 50 // used. 51 PrivateKeyID string 52 53 // Subject is the optional user to impersonate. 54 Subject string 55 56 // Scopes optionally specifies a list of requested permission scopes. 57 Scopes []string 58 59 // TokenURL is the endpoint required to complete the 2-legged JWT flow. 60 TokenURL string 61 62 // Expires optionally specifies how long the token is valid for. 63 Expires time.Duration 64 65 // Audience optionally specifies the intended audience of the 66 // request. If empty, the value of TokenURL is used as the 67 // intended audience. 68 Audience string 69 70 // PrivateClaims optionally specifies custom private claims in the JWT. 71 // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 72 PrivateClaims map[string]interface{} 73 74 // UseIDToken optionally specifies whether ID token should be used instead 75 // of access token when the server returns both. 76 UseIDToken bool 77} 78 79// TokenSource returns a JWT TokenSource using the configuration 80// in c and the HTTP client from the provided context. 81func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 82 return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) 83} 84 85// Client returns an HTTP client wrapping the context's 86// HTTP transport and adding Authorization headers with tokens 87// obtained from c. 88// 89// The returned client and its Transport should not be modified. 90func (c *Config) Client(ctx context.Context) *http.Client { 91 return oauth2.NewClient(ctx, c.TokenSource(ctx)) 92} 93 94// jwtSource is a source that always does a signed JWT request for a token. 95// It should typically be wrapped with a reuseTokenSource. 96type jwtSource struct { 97 ctx context.Context 98 conf *Config 99} 100 101func (js jwtSource) Token() (*oauth2.Token, error) { 102 pk, err := internal.ParseKey(js.conf.PrivateKey) 103 if err != nil { 104 return nil, err 105 } 106 hc := oauth2.NewClient(js.ctx, nil) 107 claimSet := &jws.ClaimSet{ 108 Iss: js.conf.Email, 109 Scope: strings.Join(js.conf.Scopes, " "), 110 Aud: js.conf.TokenURL, 111 PrivateClaims: js.conf.PrivateClaims, 112 } 113 if subject := js.conf.Subject; subject != "" { 114 claimSet.Sub = subject 115 // prn is the old name of sub. Keep setting it 116 // to be compatible with legacy OAuth 2.0 providers. 117 claimSet.Prn = subject 118 } 119 if t := js.conf.Expires; t > 0 { 120 claimSet.Exp = time.Now().Add(t).Unix() 121 } 122 if aud := js.conf.Audience; aud != "" { 123 claimSet.Aud = aud 124 } 125 h := *defaultHeader 126 h.KeyID = js.conf.PrivateKeyID 127 payload, err := jws.Encode(&h, claimSet, pk) 128 if err != nil { 129 return nil, err 130 } 131 v := url.Values{} 132 v.Set("grant_type", defaultGrantType) 133 v.Set("assertion", payload) 134 resp, err := hc.PostForm(js.conf.TokenURL, v) 135 if err != nil { 136 return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 137 } 138 defer resp.Body.Close() 139 body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) 140 if err != nil { 141 return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 142 } 143 if c := resp.StatusCode; c < 200 || c > 299 { 144 return nil, &oauth2.RetrieveError{ 145 Response: resp, 146 Body: body, 147 } 148 } 149 // tokenRes is the JSON response body. 150 var tokenRes struct { 151 AccessToken string `json:"access_token"` 152 TokenType string `json:"token_type"` 153 IDToken string `json:"id_token"` 154 ExpiresIn int64 `json:"expires_in"` // relative seconds from now 155 } 156 if err := json.Unmarshal(body, &tokenRes); err != nil { 157 return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) 158 } 159 token := &oauth2.Token{ 160 AccessToken: tokenRes.AccessToken, 161 TokenType: tokenRes.TokenType, 162 } 163 raw := make(map[string]interface{}) 164 json.Unmarshal(body, &raw) // no error checks for optional fields 165 token = token.WithExtra(raw) 166 167 if secs := tokenRes.ExpiresIn; secs > 0 { 168 token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) 169 } 170 if v := tokenRes.IDToken; v != "" { 171 // decode returned id token to get expiry 172 claimSet, err := jws.Decode(v) 173 if err != nil { 174 return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) 175 } 176 token.Expiry = time.Unix(claimSet.Exp, 0) 177 } 178 if js.conf.UseIDToken { 179 if tokenRes.IDToken == "" { 180 return nil, fmt.Errorf("oauth2: response doesn't have JWT token") 181 } 182 token.AccessToken = tokenRes.IDToken 183 } 184 return token, nil 185} 186