Skip to content

Commit a2e1a20

Browse files
jprobinsonhiranya911
authored andcommitted
Enabling the use of App Engine's SignBytes, ServiceAccount and PublicCertificates (#18)
* Release 1.0.1 (#16) * Using the ClientOptions provided at App initialization to create the … (#12) * Using the ClientOptions provided at App initialization to create the HTTPClient in auth package. * Fixed context import * Updated test case * Fixing a test failure; Calling transport.NewHTTPClient() only when ctx and opts are available to avoid an unnecessary default credentials lookup. * Passing a non-nil context to AuthConfig during testing; Replacing Print+Exit calls with log.Fatal() (#13) * Bumped version to 1.0.1 (#15) * adding native app engine support via build tags * missing files * adding ability to use standard signer locally with GAE * allowing for std signer on GAE production env, fixing opts check * fixing env var name * removing test log * giving the key source the same treatment as the signer * missing files * removing unneeded build tag * code review feedback * missing file * adjusting constructor per review feedback * file rename per feedback * reverting more changes * review feedback, fixing test
1 parent 3c2627f commit a2e1a20

File tree

6 files changed

+210
-59
lines changed

6 files changed

+210
-59
lines changed

auth/auth.go

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@
1616
package auth
1717

1818
import (
19+
"crypto/rsa"
20+
"crypto/x509"
1921
"encoding/json"
2022
"encoding/pem"
2123
"errors"
2224
"fmt"
2325
"strings"
2426

25-
"crypto/rsa"
26-
"crypto/x509"
27-
2827
"firebase.google.com/go/internal"
2928
)
3029

@@ -62,45 +61,60 @@ type Token struct {
6261
type Client struct {
6362
ks keySource
6463
projectID string
65-
email string
66-
pk *rsa.PrivateKey
64+
snr signer
65+
}
66+
67+
type signer interface {
68+
Email() (string, error)
69+
Sign(b []byte) ([]byte, error)
6770
}
6871

6972
// NewClient creates a new instance of the Firebase Auth Client.
7073
//
7174
// This function can only be invoked from within the SDK. Client applications should access the
7275
// the Auth service through firebase.App.
7376
func NewClient(c *internal.AuthConfig) (*Client, error) {
77+
var (
78+
err error
79+
email string
80+
pk *rsa.PrivateKey
81+
)
82+
if c.Creds != nil && len(c.Creds.JSON) > 0 {
83+
var svcAcct struct {
84+
ClientEmail string `json:"client_email"`
85+
PrivateKey string `json:"private_key"`
86+
}
87+
if err := json.Unmarshal(c.Creds.JSON, &svcAcct); err != nil {
88+
return nil, err
89+
}
90+
if svcAcct.PrivateKey != "" {
91+
pk, err = parseKey(svcAcct.PrivateKey)
92+
if err != nil {
93+
return nil, err
94+
}
95+
}
96+
email = svcAcct.ClientEmail
97+
}
98+
var snr signer
99+
if email != "" && pk != nil {
100+
snr = serviceAcctSigner{email: email, pk: pk}
101+
} else {
102+
snr, err = newSigner(c.Ctx)
103+
if err != nil {
104+
return nil, err
105+
}
106+
}
107+
74108
ks, err := newHTTPKeySource(c.Ctx, googleCertURL, c.Opts...)
75109
if err != nil {
76110
return nil, err
77111
}
78112

79-
client := &Client{
113+
return &Client{
80114
ks: ks,
81115
projectID: c.ProjectID,
82-
}
83-
if c.Creds == nil || len(c.Creds.JSON) == 0 {
84-
return client, nil
85-
}
86-
87-
var svcAcct struct {
88-
ClientEmail string `json:"client_email"`
89-
PrivateKey string `json:"private_key"`
90-
}
91-
if err := json.Unmarshal(c.Creds.JSON, &svcAcct); err != nil {
92-
return nil, err
93-
}
94-
95-
if svcAcct.PrivateKey != "" {
96-
pk, err := parseKey(svcAcct.PrivateKey)
97-
if err != nil {
98-
return nil, err
99-
}
100-
client.pk = pk
101-
}
102-
client.email = svcAcct.ClientEmail
103-
return client, nil
116+
snr: snr,
117+
}, nil
104118
}
105119

106120
// CustomToken creates a signed custom authentication token with the specified user ID. The resulting
@@ -114,11 +128,9 @@ func (c *Client) CustomToken(uid string) (string, error) {
114128
// CustomTokenWithClaims is similar to CustomToken, but in addition to the user ID, it also encodes
115129
// all the key-value pairs in the provided map as claims in the resulting JWT.
116130
func (c *Client) CustomTokenWithClaims(uid string, devClaims map[string]interface{}) (string, error) {
117-
if c.email == "" {
118-
return "", errors.New("service account email not available")
119-
}
120-
if c.pk == nil {
121-
return "", errors.New("private key not available")
131+
iss, err := c.snr.Email()
132+
if err != nil {
133+
return "", err
122134
}
123135

124136
if len(uid) == 0 || len(uid) > 128 {
@@ -139,15 +151,15 @@ func (c *Client) CustomTokenWithClaims(uid string, devClaims map[string]interfac
139151

140152
now := clk.Now().Unix()
141153
payload := &customToken{
142-
Iss: c.email,
143-
Sub: c.email,
154+
Iss: iss,
155+
Sub: iss,
144156
Aud: firebaseAudience,
145157
UID: uid,
146158
Iat: now,
147159
Exp: now + tokenExpSeconds,
148160
Claims: devClaims,
149161
}
150-
return encodeToken(defaultHeader(), payload, c.pk)
162+
return encodeToken(c.snr, defaultHeader(), payload)
151163
}
152164

153165
// VerifyIDToken verifies the signature and payload of the provided ID token.

auth/auth_appengine.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// +build appengine
2+
3+
// Copyright 2017 Google Inc. All Rights Reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package auth
18+
19+
import (
20+
"golang.org/x/net/context"
21+
22+
"google.golang.org/appengine"
23+
)
24+
25+
type aeSigner struct {
26+
ctx context.Context
27+
}
28+
29+
func newSigner(ctx context.Context) (signer, error) {
30+
return aeSigner{ctx}, nil
31+
}
32+
33+
func (s aeSigner) Email() (string, error) {
34+
return appengine.ServiceAccount(s.ctx)
35+
}
36+
37+
func (s aeSigner) Sign(ss []byte) ([]byte, error) {
38+
_, sig, err := appengine.SignBytes(s.ctx, ss)
39+
return sig, err
40+
}

auth/auth_std.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// +build !appengine
2+
3+
// Copyright 2017 Google Inc. All Rights Reserved.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package auth
18+
19+
import "context"
20+
21+
func newSigner(ctx context.Context) (signer, error) {
22+
return serviceAcctSigner{}, nil
23+
}

auth/auth_test.go

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727

2828
"google.golang.org/api/option"
2929
"google.golang.org/api/transport"
30+
"google.golang.org/appengine"
31+
"google.golang.org/appengine/aetest"
3032

3133
"firebase.google.com/go/internal"
3234
)
@@ -75,7 +77,10 @@ func getIDTokenWithKid(kid string, p mockIDTokenPayload) string {
7577
}
7678
h := defaultHeader()
7779
h.KeyID = kid
78-
token, _ := encodeToken(h, pCopy, client.pk)
80+
token, err := encodeToken(client.snr, h, pCopy)
81+
if err != nil {
82+
log.Fatalln(err)
83+
}
7984
return token
8085
}
8186

@@ -95,22 +100,44 @@ func (t *mockKeySource) Keys() ([]*publicKey, error) {
95100
}
96101

97102
func TestMain(m *testing.M) {
98-
var err error
99-
opt := option.WithCredentialsFile("../testdata/service_account.json")
100-
creds, err = transport.Creds(context.Background(), opt)
101-
if err != nil {
102-
log.Fatalln(err)
103+
var (
104+
err error
105+
ks keySource
106+
ctx context.Context
107+
creds *google.DefaultCredentials
108+
)
109+
110+
if appengine.IsDevAppServer() {
111+
aectx, aedone, err := aetest.NewContext()
112+
if err != nil {
113+
log.Fatalln(err)
114+
}
115+
ctx = aectx
116+
defer aedone()
117+
118+
ks, err = newAEKeySource(ctx)
119+
if err != nil {
120+
log.Fatalln(err)
121+
}
122+
} else {
123+
opt := option.WithCredentialsFile("../testdata/service_account.json")
124+
creds, err = transport.Creds(context.Background(), opt)
125+
if err != nil {
126+
log.Fatalln(err)
127+
}
128+
129+
ks = &fileKeySource{FilePath: "../testdata/public_certs.json"}
103130
}
104131

105132
client, err = NewClient(&internal.AuthConfig{
106-
Ctx: context.Background(),
133+
Ctx: ctx,
107134
Creds: creds,
108135
ProjectID: "mock-project-id",
109136
})
110137
if err != nil {
111138
log.Fatalln(err)
112139
}
113-
client.ks = &fileKeySource{FilePath: "../testdata/public_certs.json"}
140+
client.ks = ks
114141

115142
testIDToken = getIDToken(nil)
116143
os.Exit(m.Run())
@@ -259,3 +286,28 @@ func TestCertificateRequestError(t *testing.T) {
259286
t.Error("VeridyIDToken() = nil; want error")
260287
}
261288
}
289+
290+
type aeKeySource struct {
291+
keys []*publicKey
292+
}
293+
294+
func newAEKeySource(ctx context.Context) (keySource, error) {
295+
certs, err := appengine.PublicCertificates(ctx)
296+
if err != nil {
297+
return nil, err
298+
}
299+
keys := make([]*publicKey, len(certs))
300+
for i, cert := range certs {
301+
pk, err := parsePublicKey("mock-key-id-1", cert.Data)
302+
if err != nil {
303+
return nil, err
304+
}
305+
keys[i] = pk
306+
}
307+
return aeKeySource{keys}, nil
308+
}
309+
310+
// Keys returns the RSA Public Keys managed by App Engine.
311+
func (k aeKeySource) Keys() ([]*publicKey, error) {
312+
return k.keys, nil
313+
}

auth/crypto.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package auth
1616

1717
import (
1818
"crypto"
19+
"crypto/rand"
1920
"crypto/rsa"
2021
"crypto/sha256"
2122
"crypto/x509"
@@ -189,20 +190,28 @@ func parsePublicKeys(keys []byte) ([]*publicKey, error) {
189190

190191
var result []*publicKey
191192
for kid, key := range m {
192-
block, _ := pem.Decode([]byte(key))
193-
cert, err := x509.ParseCertificate(block.Bytes)
193+
pubKey, err := parsePublicKey(kid, []byte(key))
194194
if err != nil {
195195
return nil, err
196196
}
197-
pk, ok := cert.PublicKey.(*rsa.PublicKey)
198-
if !ok {
199-
return nil, errors.New("Certificate is not a RSA key")
200-
}
201-
result = append(result, &publicKey{kid, pk})
197+
result = append(result, pubKey)
202198
}
203199
return result, nil
204200
}
205201

202+
func parsePublicKey(kid string, key []byte) (*publicKey, error) {
203+
block, _ := pem.Decode(key)
204+
cert, err := x509.ParseCertificate(block.Bytes)
205+
if err != nil {
206+
return nil, err
207+
}
208+
pk, ok := cert.PublicKey.(*rsa.PublicKey)
209+
if !ok {
210+
return nil, errors.New("Certificate is not a RSA key")
211+
}
212+
return &publicKey{kid, pk}, nil
213+
}
214+
206215
func verifySignature(parts []string, k *publicKey) error {
207216
content := parts[0] + "." + parts[1]
208217
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
@@ -214,3 +223,24 @@ func verifySignature(parts []string, k *publicKey) error {
214223
h.Write([]byte(content))
215224
return rsa.VerifyPKCS1v15(k.Key, crypto.SHA256, h.Sum(nil), []byte(signature))
216225
}
226+
227+
type serviceAcctSigner struct {
228+
email string
229+
pk *rsa.PrivateKey
230+
}
231+
232+
func (s serviceAcctSigner) Email() (string, error) {
233+
if s.email == "" {
234+
return "", errors.New("service account email not available")
235+
}
236+
return s.email, nil
237+
}
238+
239+
func (s serviceAcctSigner) Sign(ss []byte) ([]byte, error) {
240+
if s.pk == nil {
241+
return nil, errors.New("private key not available")
242+
}
243+
hash := sha256.New()
244+
hash.Write([]byte(ss))
245+
return rsa.SignPKCS1v15(rand.Reader, s.pk, crypto.SHA256, hash.Sum(nil))
246+
}

0 commit comments

Comments
 (0)