Skip to content

Commit 592fb18

Browse files
Initial commit
0 parents  commit 592fb18

19 files changed

+995
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app/firebase-credentials.json

app/app.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/captaincodeman/go-firebase"
7+
"github.com/rs/cors"
8+
"google.golang.org/appengine"
9+
"google.golang.org/appengine/log"
10+
)
11+
12+
var auth *firebase.Auth
13+
14+
func init() {
15+
// default firebase app, uses firebase-credentials.json file
16+
fb, _ := firebase.New()
17+
auth = fb.Auth()
18+
19+
// for calling remotely from our front-end
20+
c := cors.New(cors.Options{
21+
AllowedOrigins: []string{"*"},
22+
AllowedHeaders: []string{"Authorization"},
23+
})
24+
mux := c.Handler(http.HandlerFunc(handler))
25+
http.Handle("/", mux)
26+
}
27+
28+
func handler(w http.ResponseWriter, r *http.Request) {
29+
ctx := appengine.NewContext(r)
30+
31+
// allow authorization token to be sent in querystring (which
32+
// would avoid a CORS preflight OPTIONS request) or using the
33+
// Authorization http header (in the format "Bearer token")
34+
authorization, err := firebase.AuthorizationFromRequest(r)
35+
if err != nil {
36+
log.Errorf(ctx, "AuthorizationFromRequest %v", err)
37+
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
38+
return
39+
}
40+
41+
// check that it's valid
42+
token, err := auth.VerifyIDToken(ctx, authorization)
43+
if err != nil {
44+
log.Errorf(ctx, "VerifyIDToken %v", err)
45+
http.Error(w, err.Error(), http.StatusBadRequest)
46+
return
47+
}
48+
49+
// get the firebase user id
50+
userID, _ := token.UID()
51+
52+
// Here is where we'd lookup the user and set the custom claims
53+
// that we want to be added to the token we're going to produce
54+
developerClaims := make(firebase.Claims)
55+
developerClaims["uid"] = 1 // our internal system id
56+
developerClaims["roles"] = []string{
57+
"admin",
58+
"operator",
59+
}
60+
61+
// mint a custom token
62+
tokenString, err := auth.CreateCustomToken(userID, &developerClaims)
63+
if err != nil {
64+
log.Errorf(ctx, "CreateCustomToken %v", err)
65+
http.Error(w, err.Error(), http.StatusInternalServerError)
66+
return
67+
}
68+
69+
// TODO: set headers for no-cacheability
70+
71+
// write it as text
72+
w.Write([]byte(tokenString))
73+
}

app/app.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
project: captain-codeman
2+
service: auth
3+
version: 20161120
4+
runtime: go
5+
api_version: go1
6+
7+
handlers:
8+
- url: /.*
9+
script: _go_app

appengine_hook.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// +build appengine
2+
3+
// App Engine hooks.
4+
5+
package firebase
6+
7+
import (
8+
"net/http"
9+
10+
"golang.org/x/net/context"
11+
"google.golang.org/appengine/urlfetch"
12+
)
13+
14+
func init() {
15+
RegisterContextClientFunc(contextClientAppEngine)
16+
}
17+
18+
func contextClientAppEngine(ctx context.Context) (*http.Client, error) {
19+
return urlfetch.Client(ctx), nil
20+
}

auth.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package firebase
2+
3+
import (
4+
"time"
5+
6+
"github.com/SermoDigital/jose/jwt"
7+
)
8+
9+
const (
10+
// Audience to use for Firebase Auth Custom tokens
11+
firebaseAudienceURL = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"
12+
13+
// expiry leeway.
14+
acceptableExpSkew = 300 * time.Second
15+
)
16+
17+
type (
18+
claims struct {
19+
jwt.Claims
20+
}
21+
22+
Auth struct {
23+
app *App
24+
}
25+
)

certs.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package firebase
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strconv"
7+
"strings"
8+
"sync"
9+
"time"
10+
11+
"crypto/x509"
12+
"encoding/json"
13+
"encoding/pem"
14+
"net/http"
15+
16+
"golang.org/x/net/context"
17+
)
18+
19+
const (
20+
defaultCertsCacheTime = 1 * time.Hour
21+
22+
// URL containing the public keys for the Google certs
23+
clientCertURL = "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]"
24+
)
25+
26+
type (
27+
certificateStore struct {
28+
sync.RWMutex
29+
url string
30+
certs map[string]*x509.Certificate
31+
exp time.Time
32+
}
33+
)
34+
35+
var (
36+
certs *certificateStore
37+
)
38+
39+
func init() {
40+
certs = newCertificateStore("")
41+
}
42+
43+
func newCertificateStore(url string) *certificateStore {
44+
if url == "" {
45+
url = clientCertURL
46+
}
47+
return &certificateStore{
48+
url: url,
49+
}
50+
}
51+
52+
func (c *certificateStore) Get(ctx context.Context, kid string) (*x509.Certificate, error) {
53+
if err := c.ensureLoaded(ctx); err != nil {
54+
return nil, err
55+
}
56+
57+
c.RLock()
58+
defer c.RUnlock()
59+
60+
cert, found := c.certs[kid]
61+
if !found {
62+
return nil, fmt.Errorf("certificate not found for key ID: %s", kid)
63+
}
64+
return cert, nil
65+
}
66+
67+
func (c *certificateStore) ensureLoaded(ctx context.Context) error {
68+
c.RLock()
69+
if c.exp.After(clock.Now()) {
70+
c.RUnlock()
71+
return nil
72+
}
73+
c.RUnlock()
74+
75+
certs, cacheTime, err := c.download(ctx)
76+
if err != nil {
77+
return err
78+
}
79+
80+
c.Lock()
81+
defer c.Unlock()
82+
c.certs = certs
83+
c.exp = clock.Now().Add(cacheTime)
84+
return nil
85+
}
86+
87+
// TODO: pass in transport, provide appengine stub to automatically get it from context
88+
func (c *certificateStore) download(ctx context.Context) (map[string]*x509.Certificate, time.Duration, error) {
89+
client, err := ContextClient(ctx)
90+
if err != nil {
91+
return nil, 0, err
92+
}
93+
94+
resp, err := client.Get(c.url)
95+
if err != nil {
96+
return nil, 0, err
97+
}
98+
defer resp.Body.Close()
99+
if resp.StatusCode != http.StatusOK {
100+
return nil, 0, fmt.Errorf("download %s fails: %s", c.url, resp.Status)
101+
}
102+
certs, err := parse(resp.Body)
103+
if err != nil {
104+
return nil, 0, err
105+
}
106+
return certs, cacheTime(resp), nil
107+
}
108+
109+
// parse parses the certificates response in JSON format.
110+
// The response has the format:
111+
// {
112+
// "kid1": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
113+
// "kid2": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
114+
// }
115+
func parse(r io.Reader) (map[string]*x509.Certificate, error) {
116+
m := make(map[string]string)
117+
dec := json.NewDecoder(r)
118+
if err := dec.Decode(&m); err != nil {
119+
return nil, err
120+
}
121+
certs := make(map[string]*x509.Certificate)
122+
for k, v := range m {
123+
block, _ := pem.Decode([]byte(v))
124+
c, err := x509.ParseCertificate(block.Bytes)
125+
if err != nil {
126+
return nil, err
127+
}
128+
certs[k] = c
129+
}
130+
return certs, nil
131+
}
132+
133+
// cacheTime extracts the cache time from the HTTP response header.
134+
// A default cache time is returned if extraction fails.
135+
func cacheTime(resp *http.Response) time.Duration {
136+
cc := strings.Split(resp.Header.Get("Cache-Control"), ",")
137+
const maxAge = "max-age="
138+
for _, c := range cc {
139+
c = strings.TrimSpace(c)
140+
if strings.HasPrefix(c, maxAge) {
141+
if d, err := strconv.Atoi(c[len(maxAge):]); err == nil {
142+
return time.Duration(d) * time.Second
143+
}
144+
}
145+
}
146+
return defaultCertsCacheTime
147+
}

certs_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package firebase
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"google.golang.org/appengine/aetest"
8+
)
9+
10+
func TestCertificateStore(t *testing.T) {
11+
ctx, done, err := aetest.NewContext()
12+
if err != nil {
13+
t.Fatal(err)
14+
}
15+
defer done()
16+
17+
store := newCertificateStore("")
18+
19+
cert, err := store.Get(ctx, "09712c9531f921fce0118dba9441de0ed4f408f7")
20+
if err != nil {
21+
t.Error(err)
22+
}
23+
t.Logf("cert 1 %v", cert.AuthorityKeyId)
24+
25+
nowFunc = func() time.Time {
26+
return time.Date(2000, 12, 15, 17, 8, 00, 0, time.UTC)
27+
}
28+
29+
cert, err = store.Get(ctx, "09712c9531f921fce0118dba9441de0ed4f408f7")
30+
if err != nil {
31+
t.Error(err)
32+
}
33+
t.Logf("cert 2 %v", cert.AuthorityKeyId)
34+
}

claims.go

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package firebase
2+
3+
// Claims to be stored in a custom token (and made available to security rules
4+
// in Database, Storage, etc.). These must be serializable to JSON
5+
// (e.g. contains only Maps, Arrays, Strings, Booleans, Numbers, etc.).
6+
type Claims map[string]interface{}

config.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package firebase
2+
3+
type (
4+
// Config stores firebase app configuration settings
5+
Config struct {
6+
Name string
7+
Credentials *Credentials
8+
CredentialsPath string
9+
}
10+
11+
// Option is the signature for configuration options
12+
Option func(*Config) error
13+
)
14+
15+
func defaultConfig() *Config {
16+
return &Config{
17+
Name: defaultAppName,
18+
CredentialsPath: "firebase-credentials.json",
19+
}
20+
}
21+
22+
// WithName sets the name of the app
23+
func WithName(name string) func(*Config) error {
24+
return func(c *Config) error {
25+
c.Name = normalizeName(name)
26+
return nil
27+
}
28+
}
29+
30+
// WithCredentialsPath sets the path to load credentials from
31+
func WithCredentialsPath(path string) func(*Config) error {
32+
return func(c *Config) error {
33+
c.CredentialsPath = path
34+
return nil
35+
}
36+
}
37+
38+
// WithCredentials sets the credentials
39+
func WithCredentials(creds *Credentials) func(*Config) error {
40+
return func(c *Config) error {
41+
c.Credentials = creds
42+
return nil
43+
}
44+
}

0 commit comments

Comments
 (0)