Skip to content

Commit d23313a

Browse files
authored
Consolidating the Two API Proposals into One (#5)
* Dropped app and credentials packages; Implemented admin package as a combination of those 2 * Adding a test case to verify token sources work * Added more tests
1 parent f317498 commit d23313a

16 files changed

+684
-1107
lines changed

admin.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Package admin is the entry point to the Firebase Admin SDK. It provides functionality for initializing and managing
2+
// App instances, which serve as central entities that provide access to various other Firebase services exposed from
3+
// the SDK.
4+
package admin
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"errors"
10+
"fmt"
11+
12+
"github.com/firebase/firebase-admin-go/auth"
13+
"github.com/firebase/firebase-admin-go/internal"
14+
15+
"io/ioutil"
16+
17+
"os"
18+
19+
"golang.org/x/oauth2"
20+
"golang.org/x/oauth2/google"
21+
"golang.org/x/oauth2/jwt"
22+
"google.golang.org/api/option"
23+
)
24+
25+
var firebaseScopes = []string{
26+
"https://www.googleapis.com/auth/firebase",
27+
"https://www.googleapis.com/auth/userinfo.email",
28+
}
29+
30+
// An App holds configuration and state common to all Firebase services that are exposed from the SDK.
31+
type App struct {
32+
ctx context.Context
33+
jwtConf *jwt.Config
34+
projectID string
35+
opts []option.ClientOption
36+
}
37+
38+
// Config represents the configuration used to initialize an App.
39+
type Config struct {
40+
ProjectID string
41+
}
42+
43+
// Auth returns an instance of auth.Client.
44+
func (a *App) Auth() (*auth.Client, error) {
45+
conf := &internal.AuthConfig{
46+
Config: a.jwtConf,
47+
ProjectID: a.projectID,
48+
}
49+
return auth.NewClient(conf)
50+
}
51+
52+
// AppFromServiceAcctFile creates a new App from the provided service account JSON file.
53+
//
54+
// This calls AppFromServiceAcct internally, with the JSON bytes read from the specified file.
55+
// Service account JSON files (also known as service account private keys) can be downloaded from the
56+
// "Settings" tab of a Firebase project in the Firebase console (https://console.firebase.google.com). See
57+
// https://firebase.google.com/docs/admin/setup for code samples and detailed documentation.
58+
func AppFromServiceAcctFile(ctx context.Context, config *Config, file string) (*App, error) {
59+
b, err := ioutil.ReadFile(file)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return AppFromServiceAcct(ctx, config, b)
64+
}
65+
66+
// AppFromServiceAcct creates a new App from the provided service account JSON.
67+
//
68+
// This can be used when the service account JSON is not loaded from the local file system, but is read
69+
// from some other source.
70+
func AppFromServiceAcct(ctx context.Context, config *Config, bytes []byte) (*App, error) {
71+
if config == nil {
72+
config = &Config{}
73+
}
74+
75+
jc, err := google.JWTConfigFromJSON(bytes, firebaseScopes...)
76+
if err != nil {
77+
return nil, err
78+
}
79+
if jc.Email == "" {
80+
return nil, errors.New("'client_email' field not available")
81+
} else if jc.TokenURL == "" {
82+
return nil, errors.New("'token_uri' field not available")
83+
} else if jc.PrivateKey == nil {
84+
return nil, errors.New("'private_key' field not available")
85+
} else if jc.PrivateKeyID == "" {
86+
return nil, errors.New("'private_key_id' field not available")
87+
}
88+
89+
pid := config.ProjectID
90+
if pid == "" {
91+
s := &struct {
92+
ProjectID string `json:"project_id"`
93+
}{}
94+
if err := json.Unmarshal(bytes, s); err != nil {
95+
return nil, err
96+
}
97+
pid = projectID(s.ProjectID)
98+
}
99+
100+
opts := []option.ClientOption{
101+
option.WithScopes(firebaseScopes...),
102+
option.WithTokenSource(jc.TokenSource(ctx)),
103+
}
104+
return &App{
105+
ctx: ctx,
106+
jwtConf: jc,
107+
projectID: pid,
108+
opts: opts,
109+
}, nil
110+
}
111+
112+
// AppFromRefreshTokenFile creates a new App from the provided refresh token JSON file.
113+
//
114+
// The JSON file must contain refresh_token, client_id and client_secret fields in addition to a type
115+
// field set to the value "authorized_user". These files are usually created and managed by the Google Cloud SDK.
116+
// This function calls AppFromRefreshToken internally, with the JSON bytes read from the specified file.
117+
func AppFromRefreshTokenFile(ctx context.Context, config *Config, file string) (*App, error) {
118+
b, err := ioutil.ReadFile(file)
119+
if err != nil {
120+
return nil, err
121+
}
122+
return AppFromRefreshToken(ctx, config, b)
123+
}
124+
125+
// AppFromRefreshToken creates a new App from the provided refresh token JSON.
126+
//
127+
// The refresh token JSON must contain refresh_token, client_id and client_secret fields in addition to a type
128+
// field set to the value "authorized_user".
129+
func AppFromRefreshToken(ctx context.Context, config *Config, bytes []byte) (*App, error) {
130+
if config == nil {
131+
config = &Config{}
132+
}
133+
rt := &struct {
134+
Type string `json:"type"`
135+
ClientSecret string `json:"client_secret"`
136+
ClientID string `json:"client_id"`
137+
RefreshToken string `json:"refresh_token"`
138+
}{}
139+
if err := json.Unmarshal(bytes, rt); err != nil {
140+
return nil, err
141+
}
142+
if rt.Type != "authorized_user" {
143+
return nil, fmt.Errorf("'type' field is %q (expected %q)", rt.Type, "authorized_user")
144+
} else if rt.ClientID == "" {
145+
return nil, fmt.Errorf("'client_id' field not available")
146+
} else if rt.ClientSecret == "" {
147+
return nil, fmt.Errorf("'client_secret' field not available")
148+
} else if rt.RefreshToken == "" {
149+
return nil, fmt.Errorf("'refresh_token' field not available")
150+
}
151+
oc := &oauth2.Config{
152+
ClientID: rt.ClientID,
153+
ClientSecret: rt.ClientSecret,
154+
Endpoint: google.Endpoint,
155+
Scopes: firebaseScopes,
156+
}
157+
token := &oauth2.Token{
158+
RefreshToken: rt.RefreshToken,
159+
}
160+
161+
opts := []option.ClientOption{
162+
option.WithScopes(firebaseScopes...),
163+
option.WithTokenSource(oc.TokenSource(ctx, token)),
164+
}
165+
return &App{
166+
ctx: ctx,
167+
projectID: projectID(config.ProjectID),
168+
opts: opts,
169+
}, nil
170+
}
171+
172+
// NewApp creates a new App based on the runtime environment.
173+
//
174+
// NewApp inspects the runtime environment to fetch a valid set of authentication credentials. This is
175+
// particularly useful when deployed in a managed cloud environment such as Google App Engine or Google Compute Engine.
176+
// Refer https://developers.google.com/identity/protocols/application-default-credentials for more details on how
177+
// application default credentials work.
178+
func NewApp(ctx context.Context, config *Config, opts ...option.ClientOption) (*App, error) {
179+
if config == nil {
180+
config = &Config{}
181+
}
182+
183+
// TODO: Use creds.Get() when it's available.
184+
cred, err := google.FindDefaultCredentials(ctx, firebaseScopes...)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
pid := config.ProjectID
190+
o := []option.ClientOption{option.WithScopes(firebaseScopes...)}
191+
if cred != nil {
192+
if pid == "" {
193+
pid = projectID(cred.ProjectID)
194+
}
195+
196+
o = append(o, option.WithTokenSource(cred.TokenSource))
197+
}
198+
o = append(o, opts...)
199+
200+
var jc *jwt.Config
201+
// TODO: Needs changes from Chris to make the following work.
202+
// jc := cred.JWTConfig
203+
204+
return &App{
205+
ctx: ctx,
206+
jwtConf: jc,
207+
projectID: pid,
208+
opts: o,
209+
}, nil
210+
}
211+
212+
func projectID(def string) string {
213+
if def == "" {
214+
return os.Getenv("GCLOUD_PROJECT")
215+
}
216+
return def
217+
}

0 commit comments

Comments
 (0)