Skip to content

Commit d040287

Browse files
committed
google: support scopes for JWT access token
Change-Id: I11acd87a56cd003fdb68a5a687e37df450c400d1 GitHub-Last-Rev: efb2e8a GitHub-Pull-Request: #504 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/327929 Trust: Shin Fan <shinfan@google.com> Trust: Cody Oss <codyoss@google.com> Run-TryBot: Shin Fan <shinfan@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Cody Oss <codyoss@google.com>
1 parent f6687ab commit d040287

File tree

2 files changed

+115
-19
lines changed

2 files changed

+115
-19
lines changed

‎google/jwt.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package google
77
import (
88
"crypto/rsa"
99
"fmt"
10+
"strings"
1011
"time"
1112

1213
"golang.org/x/oauth2"
@@ -24,6 +25,28 @@ import (
2425
// optimization supported by a few Google services.
2526
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
2627
func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) {
28+
return newJWTSource(jsonKey, audience, nil)
29+
}
30+
31+
// JWTAccessTokenSourceWithScope uses a Google Developers service account JSON
32+
// key file to read the credentials that authorize and authenticate the
33+
// requests, and returns a TokenSource that does not use any OAuth2 flow but
34+
// instead creates a JWT and sends that as the access token.
35+
// The scope is typically a list of URLs that specifies the scope of the
36+
// credentials.
37+
//
38+
// Note that this is not a standard OAuth flow, but rather an
39+
// optimization supported by a few Google services.
40+
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
41+
func JWTAccessTokenSourceWithScope(jsonKey []byte, scope ...string) (oauth2.TokenSource, error) {
42+
return newJWTSource(jsonKey, "", scope)
43+
}
44+
45+
func newJWTSource(jsonKey []byte, audience string, scopes []string) (oauth2.TokenSource, error) {
46+
if len(scopes) == 0 && audience == "" {
47+
return nil, fmt.Errorf("google: missing scope/audience for JWT access token")
48+
}
49+
2750
cfg, err := JWTConfigFromJSON(jsonKey)
2851
if err != nil {
2952
return nil, fmt.Errorf("google: could not parse JSON key: %v", err)
@@ -35,6 +58,7 @@ func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.Token
3558
ts := &jwtAccessTokenSource{
3659
email: cfg.Email,
3760
audience: audience,
61+
scopes: scopes,
3862
pk: pk,
3963
pkID: cfg.PrivateKeyID,
4064
}
@@ -47,19 +71,22 @@ func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.Token
4771

4872
type jwtAccessTokenSource struct {
4973
email, audience string
74+
scopes []string
5075
pk *rsa.PrivateKey
5176
pkID string
5277
}
5378

5479
func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) {
5580
iat := time.Now()
5681
exp := iat.Add(time.Hour)
82+
scope := strings.Join(ts.scopes, " ")
5783
cs := &jws.ClaimSet{
58-
Iss: ts.email,
59-
Sub: ts.email,
60-
Aud: ts.audience,
61-
Iat: iat.Unix(),
62-
Exp: exp.Unix(),
84+
Iss: ts.email,
85+
Sub: ts.email,
86+
Aud: ts.audience,
87+
Scope: scope,
88+
Iat: iat.Unix(),
89+
Exp: exp.Unix(),
6390
}
6491
hdr := &jws.Header{
6592
Algorithm: "RS256",

‎google/jwt_test.go

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,81 @@ import (
1313
"encoding/json"
1414
"encoding/pem"
1515
"strings"
16+
"sync"
1617
"testing"
1718
"time"
1819

1920
"golang.org/x/oauth2/jws"
2021
)
2122

23+
var (
24+
privateKey *rsa.PrivateKey
25+
jsonKey []byte
26+
once sync.Once
27+
)
28+
2229
func TestJWTAccessTokenSourceFromJSON(t *testing.T) {
23-
// Generate a key we can use in the test data.
24-
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
30+
setupDummyKey(t)
31+
32+
ts, err := JWTAccessTokenSourceFromJSON(jsonKey, "audience")
2533
if err != nil {
26-
t.Fatal(err)
34+
t.Fatalf("JWTAccessTokenSourceFromJSON: %v\nJSON: %s", err, string(jsonKey))
2735
}
2836

29-
// Encode the key and substitute into our example JSON.
30-
enc := pem.EncodeToMemory(&pem.Block{
31-
Type: "PRIVATE KEY",
32-
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
33-
})
34-
enc, err = json.Marshal(string(enc))
37+
tok, err := ts.Token()
3538
if err != nil {
36-
t.Fatalf("json.Marshal: %v", err)
39+
t.Fatalf("Token: %v", err)
3740
}
38-
jsonKey := bytes.Replace(jwtJSONKey, []byte(`"super secret key"`), enc, 1)
3941

40-
ts, err := JWTAccessTokenSourceFromJSON(jsonKey, "audience")
42+
if got, want := tok.TokenType, "Bearer"; got != want {
43+
t.Errorf("TokenType = %q, want %q", got, want)
44+
}
45+
if got := tok.Expiry; tok.Expiry.Before(time.Now()) {
46+
t.Errorf("Expiry = %v, should not be expired", got)
47+
}
48+
49+
err = jws.Verify(tok.AccessToken, &privateKey.PublicKey)
4150
if err != nil {
42-
t.Fatalf("JWTAccessTokenSourceFromJSON: %v\nJSON: %s", err, string(jsonKey))
51+
t.Errorf("jws.Verify on AccessToken: %v", err)
52+
}
53+
54+
claim, err := jws.Decode(tok.AccessToken)
55+
if err != nil {
56+
t.Fatalf("jws.Decode on AccessToken: %v", err)
57+
}
58+
59+
if got, want := claim.Iss, "gopher@developer.gserviceaccount.com"; got != want {
60+
t.Errorf("Iss = %q, want %q", got, want)
61+
}
62+
if got, want := claim.Sub, "gopher@developer.gserviceaccount.com"; got != want {
63+
t.Errorf("Sub = %q, want %q", got, want)
64+
}
65+
if got, want := claim.Aud, "audience"; got != want {
66+
t.Errorf("Aud = %q, want %q", got, want)
67+
}
68+
69+
// Finally, check the header private key.
70+
parts := strings.Split(tok.AccessToken, ".")
71+
hdrJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
72+
if err != nil {
73+
t.Fatalf("base64 DecodeString: %v\nString: %q", err, parts[0])
74+
}
75+
var hdr jws.Header
76+
if err := json.Unmarshal([]byte(hdrJSON), &hdr); err != nil {
77+
t.Fatalf("json.Unmarshal: %v (%q)", err, hdrJSON)
78+
}
79+
80+
if got, want := hdr.KeyID, "268f54e43a1af97cfc71731688434f45aca15c8b"; got != want {
81+
t.Errorf("Header KeyID = %q, want %q", got, want)
82+
}
83+
}
84+
85+
func TestJWTAccessTokenSourceWithScope(t *testing.T) {
86+
setupDummyKey(t)
87+
88+
ts, err := JWTAccessTokenSourceWithScope(jsonKey, "scope1", "scope2")
89+
if err != nil {
90+
t.Fatalf("JWTAccessTokenSourceWithScope: %v\nJSON: %s", err, string(jsonKey))
4391
}
4492

4593
tok, err := ts.Token()
@@ -70,7 +118,7 @@ func TestJWTAccessTokenSourceFromJSON(t *testing.T) {
70118
if got, want := claim.Sub, "gopher@developer.gserviceaccount.com"; got != want {
71119
t.Errorf("Sub = %q, want %q", got, want)
72120
}
73-
if got, want := claim.Aud, "audience"; got != want {
121+
if got, want := claim.Scope, "scope1 scope2"; got != want {
74122
t.Errorf("Aud = %q, want %q", got, want)
75123
}
76124

@@ -89,3 +137,24 @@ func TestJWTAccessTokenSourceFromJSON(t *testing.T) {
89137
t.Errorf("Header KeyID = %q, want %q", got, want)
90138
}
91139
}
140+
141+
func setupDummyKey(t *testing.T) {
142+
once.Do(func() {
143+
// Generate a key we can use in the test data.
144+
pk, err := rsa.GenerateKey(rand.Reader, 2048)
145+
if err != nil {
146+
t.Fatal(err)
147+
}
148+
privateKey = pk
149+
// Encode the key and substitute into our example JSON.
150+
enc := pem.EncodeToMemory(&pem.Block{
151+
Type: "PRIVATE KEY",
152+
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
153+
})
154+
enc, err = json.Marshal(string(enc))
155+
if err != nil {
156+
t.Fatalf("json.Marshal: %v", err)
157+
}
158+
jsonKey = bytes.Replace(jwtJSONKey, []byte(`"super secret key"`), enc, 1)
159+
})
160+
}

0 commit comments

Comments
 (0)