Skip to content

Commit f317498

Browse files
authored
Implemented auth package; Exposing Auth service via App (#3)
* Implemented auth package; Exposing Auth service via App * Rearranged code for clarity; Other minor updates * Fixing a bug in the HTTP key source * Fixing godoc comment
1 parent c3cb5b5 commit f317498

File tree

7 files changed

+927
-1
lines changed

7 files changed

+927
-1
lines changed

app/app.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"sync"
1010

11+
"github.com/firebase/firebase-admin-go/auth"
1112
"github.com/firebase/firebase-admin-go/credentials"
1213
"github.com/firebase/firebase-admin-go/internal"
1314
)
@@ -34,7 +35,7 @@ type App interface {
3435
// Auth returns an instance of the auth.Auth service.
3536
//
3637
// Multiple calls to Auth may return the same value. Auth panics if the App is already deleted.
37-
// Auth() auth.Auth
38+
Auth() auth.Auth
3839

3940
// Del gracefully terminates this App.
4041
//
@@ -64,6 +65,13 @@ func (a *appImpl) Credential() credentials.Credential {
6465
return a.Conf.Cred
6566
}
6667

68+
func (a *appImpl) Auth() auth.Auth {
69+
fn := func() internal.AppService {
70+
return auth.New(a.Conf).(internal.AppService)
71+
}
72+
return a.service("auth", fn).(auth.Auth)
73+
}
74+
6775
func (a *appImpl) Del() {
6876
mutex.Lock()
6977
defer mutex.Unlock()

app/app_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,39 @@ func TestAppService(t *testing.T) {
273273
t.Error("Delete: false; want: true")
274274
}
275275
}
276+
277+
func TestAuth(t *testing.T) {
278+
defer clearApps()
279+
280+
app1, err := New(&Conf{Cred: cred})
281+
if err != nil {
282+
t.Fatal(err)
283+
}
284+
app2, err := New(&Conf{Cred: cred, Name: "myApp"})
285+
if err != nil {
286+
t.Fatal(err)
287+
}
288+
289+
if s := app1.Auth(); s == nil {
290+
t.Error("Auth() = nil; want auth.Auth")
291+
}
292+
if s := app2.Auth(); s == nil {
293+
t.Error("Auth() = nil; want auth.Auth")
294+
}
295+
}
296+
297+
func TestAuthAfterDeleteApp(t *testing.T) {
298+
defer clearApps()
299+
defer func() {
300+
if r := recover(); r == nil {
301+
t.Errorf("Auth() did not panic; want panic")
302+
}
303+
}()
304+
305+
app, err := New(&Conf{Cred: cred})
306+
if err != nil {
307+
t.Fatal(err)
308+
}
309+
app.Del()
310+
app.Auth()
311+
}

auth/auth.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package auth
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/firebase/firebase-admin-go/credentials"
10+
"github.com/firebase/firebase-admin-go/internal"
11+
)
12+
13+
const firebaseAudience = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
14+
const googleCertURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
15+
const issuerPrefix = "https://securetoken.google.com/"
16+
const gcloudProject = "GCLOUD_PROJECT"
17+
const tokenExpSeconds = 3600
18+
19+
var reservedClaims = []string{
20+
"acr", "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash",
21+
"exp", "firebase", "iat", "iss", "jti", "nbf", "nonce", "sub",
22+
}
23+
24+
var keys keySource = newHTTPKeySource(googleCertURL)
25+
26+
var clk clock = &systemClock{}
27+
28+
// Token represents a decoded Firebase ID token.
29+
//
30+
// Token provides typed accessors to the common JWT fields such as Audience (aud) and Expiry (exp).
31+
// Additionally it provides a UID field, which indicates the user ID of the account to which this token
32+
// belongs. Any additional JWT claims can be accessed via the Claims map of Token.
33+
type Token struct {
34+
Issuer string `json:"iss"`
35+
Audience string `json:"aud"`
36+
Expires int64 `json:"exp"`
37+
IssuedAt int64 `json:"iat"`
38+
Subject string `json:"sub,omitempty"`
39+
UID string `json:"uid,omitempty"`
40+
Claims map[string]interface{} `json:"-"`
41+
}
42+
43+
// Auth is the interface for the Firebase auth service.
44+
//
45+
// Auth facilitates generating custom JWT tokens for Firebase clients, and verifying ID tokens issued
46+
// by Firebase backend services.
47+
type Auth interface {
48+
// CustomToken creates a signed custom authentication token with the specified user ID. The resulting
49+
// JWT can be used in a Firebase client SDK to trigger an authentication flow.
50+
CustomToken(uid string) (string, error)
51+
52+
// CustomTokenWithClaims is similar to CustomToken, but in addition to the user ID, it also encodes
53+
// all the key-value pairs in the provided map as claims in the resulting JWT.
54+
CustomTokenWithClaims(uid string, devClaims map[string]interface{}) (string, error)
55+
56+
// VerifyIDToken verifies the signature and payload of the provided ID token.
57+
//
58+
// VerifyIDToken accepts a signed JWT token string, and verifies that it is current, issued for the
59+
// correct Firebase project, and signed by the Google Firebase services in the cloud. It returns
60+
// a Token containing the decoded claims in the input JWT.
61+
VerifyIDToken(idToken string) (*Token, error)
62+
}
63+
64+
// Signer represents an entity that can be used to sign custom JWT tokens.
65+
//
66+
// Credential implementations that intend to support custom token minting must implement this interface.
67+
type Signer interface {
68+
ServiceAcctEmail() string
69+
Sign(data string) ([]byte, error)
70+
}
71+
72+
// ProjectMember represents an entity that can be used to obtain a Firebase project ID.
73+
//
74+
// ProjectMember is used during ID token verification. The ID tokens passed to VerifyIDToken must contain the project
75+
// ID returned by this interface for them to be considered valid. Credential implementations that intend to support ID
76+
// token verification must implement this interface.
77+
type ProjectMember interface {
78+
ProjectID() string
79+
}
80+
81+
// New creates a new instance of the Firebase Auth service.
82+
//
83+
// This function can only be invoked from within the SDK. Client applications should access the
84+
// the Auth service through the apps.App interface.
85+
func New(c *internal.AppConf) Auth {
86+
return &authImpl{c.Cred, false}
87+
}
88+
89+
type authImpl struct {
90+
cred credentials.Credential
91+
deleted bool
92+
}
93+
94+
func (a *authImpl) CustomToken(uid string) (string, error) {
95+
return a.CustomTokenWithClaims(uid, make(map[string]interface{}))
96+
}
97+
98+
func (a *authImpl) CustomTokenWithClaims(uid string, devClaims map[string]interface{}) (string, error) {
99+
if a.deleted {
100+
return "", errors.New("parent Firebase app instance has been deleted")
101+
}
102+
if len(uid) == 0 || len(uid) > 128 {
103+
return "", errors.New("uid must be non-empty, and not longer than 128 characters")
104+
}
105+
106+
var disallowed []string
107+
for _, k := range reservedClaims {
108+
if _, contains := devClaims[k]; contains {
109+
disallowed = append(disallowed, k)
110+
}
111+
}
112+
if len(disallowed) == 1 {
113+
return "", fmt.Errorf("developer claim %q is reserved and cannot be specified", disallowed[0])
114+
} else if len(disallowed) > 1 {
115+
return "", fmt.Errorf("developer claims %q are reserved and cannot be specified", strings.Join(disallowed, ", "))
116+
}
117+
118+
signer, ok := a.cred.(Signer)
119+
if !ok {
120+
return "", errors.New("must initialize Firebase App with a credential that supports token signing")
121+
}
122+
123+
now := clk.Now().Unix()
124+
payload := &customToken{
125+
Iss: signer.ServiceAcctEmail(),
126+
Sub: signer.ServiceAcctEmail(),
127+
Aud: firebaseAudience,
128+
UID: uid,
129+
Iat: now,
130+
Exp: now + tokenExpSeconds,
131+
Claims: devClaims,
132+
}
133+
return encodeToken(defaultHeader(), payload, signer)
134+
}
135+
136+
func (a *authImpl) VerifyIDToken(idToken string) (*Token, error) {
137+
if a.deleted {
138+
return nil, errors.New("parent Firebase app instance has been deleted")
139+
}
140+
if idToken == "" {
141+
return nil, fmt.Errorf("ID token must be a non-empty string")
142+
}
143+
144+
var projectID string
145+
pm, ok := a.cred.(ProjectMember)
146+
if ok {
147+
projectID = pm.ProjectID()
148+
} else {
149+
projectID = os.Getenv(gcloudProject)
150+
if projectID == "" {
151+
return nil, fmt.Errorf("must initialize Firebase App with a credential that supports token "+
152+
"verification, or set your project ID as the %q environment variable to call "+
153+
"VerifyIDToken()", gcloudProject)
154+
}
155+
}
156+
157+
h := &jwtHeader{}
158+
p := &Token{}
159+
if err := decodeToken(idToken, keys, h, p); err != nil {
160+
return nil, err
161+
}
162+
163+
projectIDMsg := "Make sure the ID token comes from the same Firebase project as the credential used to" +
164+
" authenticate this SDK."
165+
verifyTokenMsg := "See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to " +
166+
"retrieve a valid ID token."
167+
issuer := issuerPrefix + projectID
168+
169+
var err error
170+
if h.KeyID == "" {
171+
if p.Audience == firebaseAudience {
172+
err = fmt.Errorf("VerifyIDToken() expects an ID token, but was given a custom token")
173+
} else {
174+
err = fmt.Errorf("ID token has no 'kid' header")
175+
}
176+
} else if h.Algorithm != "RS256" {
177+
err = fmt.Errorf("ID token has invalid incorrect algorithm. Expected 'RS256' but got %q. %s",
178+
h.Algorithm, verifyTokenMsg)
179+
} else if p.Audience != projectID {
180+
err = fmt.Errorf("ID token has invalid 'aud' (audience) claim. Expected %q but got %q. %s %s",
181+
projectID, p.Audience, projectIDMsg, verifyTokenMsg)
182+
} else if p.Issuer != issuer {
183+
err = fmt.Errorf("ID token has invalid 'iss' (issuer) claim. Expected %q but got %q. %s %s",
184+
issuer, p.Issuer, projectIDMsg, verifyTokenMsg)
185+
} else if p.IssuedAt > clk.Now().Unix() {
186+
err = fmt.Errorf("ID token issued at future timestamp: %d", p.IssuedAt)
187+
} else if p.Expires < clk.Now().Unix() {
188+
err = fmt.Errorf("ID token has expired. Expired at: %d", p.Expires)
189+
} else if p.Subject == "" {
190+
err = fmt.Errorf("ID token has empty 'sub' (subject) claim. %s", verifyTokenMsg)
191+
} else if len(p.Subject) > 128 {
192+
err = fmt.Errorf("ID token has a 'sub' (subject) claim longer than 128 characters. %s", verifyTokenMsg)
193+
}
194+
195+
if err != nil {
196+
return nil, err
197+
}
198+
p.UID = p.Subject
199+
return p, nil
200+
}
201+
202+
func (a *authImpl) Del() {
203+
a.cred = nil
204+
a.deleted = true
205+
}

0 commit comments

Comments
 (0)