Skip to content

Commit b8731e4

Browse files
authored
Added credential implementation (#1)
* Added credential implementation * Fixed refresh token tests * Improved logging errors during tests; Accepting a context in the API * Added comments
1 parent 4ed43f7 commit b8731e4

File tree

7 files changed

+482
-0
lines changed

7 files changed

+482
-0
lines changed

credentials/credentials.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Package credentials provides functions for creating authentication credentials, which can be used to initialize the
2+
// Firebase SDK.
3+
package credentials
4+
5+
import (
6+
"context"
7+
"crypto"
8+
"crypto/rand"
9+
"crypto/rsa"
10+
"crypto/sha256"
11+
"crypto/x509"
12+
"encoding/json"
13+
"encoding/pem"
14+
"errors"
15+
"fmt"
16+
"io"
17+
"io/ioutil"
18+
"time"
19+
20+
"golang.org/x/oauth2"
21+
"golang.org/x/oauth2/google"
22+
"golang.org/x/oauth2/jwt"
23+
)
24+
25+
var firebaseScopes = []string{
26+
"https://www.googleapis.com/auth/firebase",
27+
"https://www.googleapis.com/auth/userinfo.email",
28+
}
29+
30+
// Credential represents an authentication credential that can be used to initialize the Admin SDK.
31+
//
32+
// Credential provides the Admin SDK with OAuth2 access tokens to authenticate with various Firebase cloud services.
33+
// The Credential implementations are not required to cache the OAuth2 tokens, but they are free to do so.
34+
type Credential interface {
35+
// AccessToken fetches a valid, unexpired OAuth2 access token.
36+
//
37+
// AccessToken returns an access token string along with its expiry time, which allows higher-level code to
38+
// implement token caching. The returned token should be valid for authenticating against various Google and
39+
// Firebase services that the SDK needs to interact with. Generally, user applications do not have to call
40+
// AccessToken directly. The Admin SDK should manage calling AccessToken on behalf of user applications.
41+
AccessToken(ctx context.Context) (string, time.Time, error)
42+
}
43+
44+
type certificate struct {
45+
Config *jwt.Config
46+
PK *rsa.PrivateKey
47+
ProjID string
48+
}
49+
50+
func (c *certificate) AccessToken(ctx context.Context) (string, time.Time, error) {
51+
source := c.Config.TokenSource(ctx)
52+
token, err := source.Token()
53+
if err != nil {
54+
return "", time.Time{}, err
55+
}
56+
return token.AccessToken, token.Expiry, nil
57+
}
58+
59+
func (c *certificate) ServiceAcctEmail() string {
60+
return c.Config.Email
61+
}
62+
63+
func (c *certificate) Sign(data string) ([]byte, error) {
64+
h := sha256.New()
65+
h.Write([]byte(data))
66+
return rsa.SignPKCS1v15(rand.Reader, c.PK, crypto.SHA256, h.Sum(nil))
67+
}
68+
69+
func (c *certificate) ProjectID() string {
70+
return c.ProjID
71+
}
72+
73+
// NewCert creates a new Credential from the provided service account certificate JSON.
74+
//
75+
// Service account certificate JSON files (also known as service account private keys) can be downloaded from the
76+
// "Settings" tab of a Firebase project in the Firebase console (https://console.firebase.google.com). See
77+
// https://firebase.google.com/docs/admin/setup for code samples and detailed documentation.
78+
//
79+
// NewCert consumes all the content available in the provided service account certificate Reader. It is safe to close
80+
// the Reader once NewCert has returned.
81+
func NewCert(r io.Reader) (Credential, error) {
82+
b, err := ioutil.ReadAll(r)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
config, err := google.JWTConfigFromJSON(b, firebaseScopes...)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
if config.Email == "" {
93+
return nil, errors.New("'client_email' field not available")
94+
} else if config.TokenURL == "" {
95+
return nil, errors.New("'token_uri' field not available")
96+
} else if config.PrivateKey == nil {
97+
return nil, errors.New("'private_key' field not available")
98+
} else if config.PrivateKeyID == "" {
99+
return nil, errors.New("'private_key_id' field not available")
100+
}
101+
102+
s := &struct {
103+
ProjectID string `json:"project_id"`
104+
}{}
105+
if err = json.Unmarshal(b, s); err != nil {
106+
return nil, err
107+
} else if s.ProjectID == "" {
108+
return nil, errors.New("'project_id' field not available")
109+
}
110+
111+
pk, err := parseKey(config.PrivateKey)
112+
if err != nil {
113+
return nil, err
114+
}
115+
return &certificate{Config: config, PK: pk, ProjID: s.ProjectID}, nil
116+
}
117+
118+
type refreshToken struct {
119+
Config *oauth2.Config
120+
Token *oauth2.Token
121+
}
122+
123+
func (c *refreshToken) AccessToken(ctx context.Context) (string, time.Time, error) {
124+
source := c.Config.TokenSource(ctx, c.Token)
125+
token, err := source.Token()
126+
if err != nil {
127+
return "", time.Time{}, err
128+
}
129+
return token.AccessToken, token.Expiry, nil
130+
}
131+
132+
// NewRefreshToken creates a new Credential from the provided refresh token JSON.
133+
//
134+
// The refresh token JSON must contain refresh_token, client_id and client_secret fields in addition to a type
135+
// field set to the value "authorized_user". These files are usually created and managed by the Google Cloud SDK.
136+
func NewRefreshToken(r io.Reader) (Credential, error) {
137+
b, err := ioutil.ReadAll(r)
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
rt := &struct {
143+
Type string `json:"type"`
144+
ClientSecret string `json:"client_secret"`
145+
ClientID string `json:"client_id"`
146+
RefreshToken string `json:"refresh_token"`
147+
}{}
148+
if err := json.Unmarshal(b, rt); err != nil {
149+
return nil, err
150+
}
151+
if rt.Type != "authorized_user" {
152+
return nil, fmt.Errorf("'type' field is '%s' (expected 'authorized_user')", rt.Type)
153+
} else if rt.ClientID == "" {
154+
return nil, fmt.Errorf("'client_id' field not available")
155+
} else if rt.ClientSecret == "" {
156+
return nil, fmt.Errorf("'client_secret' field not available")
157+
} else if rt.RefreshToken == "" {
158+
return nil, fmt.Errorf("'refresh_token' field not available")
159+
}
160+
config := &oauth2.Config{
161+
ClientID: rt.ClientID,
162+
ClientSecret: rt.ClientSecret,
163+
Endpoint: google.Endpoint,
164+
Scopes: firebaseScopes,
165+
}
166+
token := &oauth2.Token{
167+
RefreshToken: rt.RefreshToken,
168+
}
169+
return &refreshToken{Config: config, Token: token}, nil
170+
}
171+
172+
type appDefault struct {
173+
Credential *google.DefaultCredentials
174+
}
175+
176+
func (c *appDefault) AccessToken(ctx context.Context) (string, time.Time, error) {
177+
source := c.Credential.TokenSource
178+
token, err := source.Token()
179+
if err != nil {
180+
return "", time.Time{}, err
181+
}
182+
return token.AccessToken, token.Expiry, nil
183+
}
184+
185+
// NewAppDefault creates a new Credential based on the runtime environment.
186+
//
187+
// NewAppDefault inspects the runtime environment to fetch a valid set of authentication credentials. This is
188+
// particularly useful when deployed in a managed cloud environment such as Google App Engine or Google Compute Engine.
189+
// Refer https://developers.google.com/identity/protocols/application-default-credentials for more details on how
190+
// application default credentials work.
191+
func NewAppDefault(ctx context.Context) (Credential, error) {
192+
cred, err := google.FindDefaultCredentials(ctx, firebaseScopes...)
193+
if err != nil {
194+
return nil, err
195+
}
196+
return &appDefault{Credential: cred}, nil
197+
}
198+
199+
func parseKey(key []byte) (*rsa.PrivateKey, error) {
200+
block, _ := pem.Decode(key)
201+
if block != nil {
202+
key = block.Bytes
203+
}
204+
parsedKey, err := x509.ParsePKCS8PrivateKey(key)
205+
if err != nil {
206+
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
207+
if err != nil {
208+
return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
209+
}
210+
}
211+
parsed, ok := parsedKey.(*rsa.PrivateKey)
212+
if !ok {
213+
return nil, errors.New("private key is not an RSA key")
214+
}
215+
return parsed, nil
216+
}

0 commit comments

Comments
 (0)