[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appcheck): Add App Check token verification #484

Merged
merged 20 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Initial set of app check unit tests.
All keys are borrowed from the Firebase Admin Node SDK.
  • Loading branch information
bamnet committed Feb 6, 2022
commit 95152c9907377e315cafea631acdf1f9d73ef98b
2 changes: 1 addition & 1 deletion appcheck/appcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type Client struct {
// NewClient creates a new App Check client.
bamnet marked this conversation as resolved.
Show resolved Hide resolved
func NewClient(ctx context.Context, conf *internal.AppCheckConfig) (*Client, error) {
// TODO: Add support for overriding the HTTP client using the App one.
jwks, err := keyfunc.Get(JWKSUrl, keyfunc.Options{
jwks, err := keyfunc.Get(conf.JWKSUrl, keyfunc.Options{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if keyfunc supports overriding the http client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, keyfunc.Options accepts an http.Client. How should we expose that option to developers?

bamnet marked this conversation as resolved.
Show resolved Hide resolved
Ctx: ctx,
})
if err != nil {
Expand Down
176 changes: 176 additions & 0 deletions appcheck/appcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package appcheck

import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"

"firebase.google.com/go/v4/internal"
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-cmp/cmp"
)

func TestVerifyTokenHasValidClaims(t *testing.T) {
pk, err := ioutil.ReadFile("../testdata/appcheck_pk.pem")
if err != nil {
t.Fatalf("Failed to read private key: %v", err)
}
block, _ := pem.Decode(pk)
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("Failed to parse private key: %v", err)
}

jwks, err := ioutil.ReadFile("../testdata/mock.jwks.json")
if err != nil {
t.Fatalf("Failed to read JWKS: %v", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(jwks)
}))
defer ts.Close()

conf := &internal.AppCheckConfig{
ProjectID: "project_id",
JWKSUrl: ts.URL,
}

client, err := NewClient(context.Background(), conf)
if err != nil {
t.Errorf("Error creating NewClient: %v", err)
}

type appCheckClaims struct {
Aud []string `json:"aud"`
jwt.RegisteredClaims
}

mockTime := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
jwt.TimeFunc = func() time.Time {
return mockTime
}

tokenTests := []struct {
claims *appCheckClaims
wantErr error
wantToken *VerifiedToken
}{
{
&appCheckClaims{
[]string{"projects/12345678", "projects/project_id"},
jwt.RegisteredClaims{
Issuer: "https://firebaseappcheck.googleapis.com/12345678",
Subject: "12345678:app:ID",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
}},
nil,
&VerifiedToken{
Iss: "https://firebaseappcheck.googleapis.com/12345678",
Sub: "12345678:app:ID",
Aud: []string{"projects/12345678", "projects/project_id"},
Exp: mockTime.Add(time.Hour),
Iat: mockTime,
AppID: "12345678:app:ID",
},
}, {
&appCheckClaims{
[]string{"projects/0000000", "projects/another_project_id"},
jwt.RegisteredClaims{
Issuer: "https://firebaseappcheck.googleapis.com/12345678",
Subject: "12345678:app:ID",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
}},
ErrTokenAudience,
nil,
}, {
&appCheckClaims{
[]string{"projects/12345678", "projects/project_id"},
jwt.RegisteredClaims{
Issuer: "https://not-firebaseappcheck.googleapis.com/12345678",
Subject: "12345678:app:ID",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
}},
ErrTokenIssuer,
nil,
}, {
&appCheckClaims{
[]string{"projects/12345678", "projects/project_id"},
jwt.RegisteredClaims{
Issuer: "https://firebaseappcheck.googleapis.com/12345678",
Subject: "",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
}},
ErrTokenSubject,
nil,
}, {
&appCheckClaims{
[]string{"projects/12345678", "projects/project_id"},
jwt.RegisteredClaims{
Issuer: "https://firebaseappcheck.googleapis.com/12345678",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
}},
ErrTokenSubject,
nil,
},
}

for _, tc := range tokenTests {
// Create an App Check token.
jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, tc.claims)
jwtToken.Header["kid"] = "FGQdnRlzAmKyKr6-Hg_kMQrBkj_H6i6ADnBQz4OI6BU"
token, err := jwtToken.SignedString(privateKey)
if err != nil {
t.Fatalf("error generating JWT: %v", err)
}

// Verify the token.
gotToken, gotErr := client.VerifyToken(token)
if !errors.Is(gotErr, tc.wantErr) {
t.Errorf("Expected error %v, got %v", tc.wantErr, gotErr)
continue
}
if diff := cmp.Diff(tc.wantToken, gotToken); diff != "" {
t.Errorf("VerifyToken mismatch (-want +got):\n%s", diff)
}
}
}

func TestVerifyTokenMustExist(t *testing.T) {
jwks, err := ioutil.ReadFile("../testdata/mock.jwks.json")
if err != nil {
t.Fatalf("Failed to read JWKS: %v", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(jwks)
}))
defer ts.Close()

conf := &internal.AppCheckConfig{
ProjectID: "project_id",
JWKSUrl: ts.URL,
}

client, err := NewClient(context.Background(), conf)
if err != nil {
t.Errorf("Error creating NewClient: %v", err)
}

gotToken, gotErr := client.VerifyToken("")
if gotErr == nil {
t.Errorf("Expected error, got nil")
}
if gotToken != nil {
t.Errorf("Expected nil, got token %v", gotToken)
}
}
1 change: 1 addition & 0 deletions firebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ func (a *App) Messaging(ctx context.Context) (*messaging.Client, error) {
func (a *App) AppCheck(ctx context.Context) (*appcheck.Client, error) {
conf := &internal.AppCheckConfig{
ProjectID: a.projectID,
JWKSUrl: appcheck.JWKSUrl,
bamnet marked this conversation as resolved.
Show resolved Hide resolved
}
return appcheck.NewClient(ctx, conf)
}
Expand Down
1 change: 1 addition & 0 deletions internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type MessagingConfig struct {
// AppCheckConfig represents the configuration of App Check service.
type AppCheckConfig struct {
ProjectID string
JWKSUrl string
}

// MockTokenSource is a TokenSource implementation that can be used for testing.
Expand Down
27 changes: 27 additions & 0 deletions testdata/appcheck_pk.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArFYQyEdjj43mnpXwj+3WgAE01TSYe1+XFE9mxUDShysFwtVZ
OHFSMm6kl+B3Y/O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP
92ezyCEp4MPmAPFD/tY160XGrkqApuY2/+L8eEXdkRyH2H7lCYypFC0u3DIY25Vl
q+ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk/NvwKOY4pJ/sm
99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81w
bgecd62F640scKBr3ko73L8M8UWcwgd+moKCJwIDAQABAoIBAEDPJQSMhE6KKL5e
2NbntJDy4zGC1A0hh6llqtpnZETc0w/QN/tX8ndw0IklKwD1ukPl6OOYVVhLjVVZ
ANpQ1GKuo1ETHsuKoMQwhMyQfbL41m5SdkCuSRfsENmsEiUslkuRtzlBRlRpRDR/
wxM8A4IflBFsT1IFdpC+yx8BVuwLc35iVnaGQpo/jhSDibt07j+FdOKEWkMGj+rL
sHC6cpB2NMTBl9CIDLW/eq1amBOAGtsSKqoGJvaQY/mZf7SPkRjYIfIl2PWSaduT
fmMrsYYFtHUKVOMYAD7P5RWNkS8oERucnXT3ouAECvip3Ew2JqlQc0FP7FS5CxH3
WdfvLuECgYEA8Q7rJrDOdO867s7P/lXMklbAGnuNnAZJdAEXUMIaPJi7al97F119
4DKBuF7c/dDf8CdiOvMzP8r/F8+FFx2D61xxkQNeuxo5Xjlt23OzW5EI2S6ABesZ
/3sQWqvKCGuqN7WENYF3EiKyByQ22MYXk8CE7KZuO57Aj88t6TsaNhkCgYEAtwSs
hbqKSCneC1bQ3wfSAF2kPYRrQEEa2VCLlX1Mz7zHufxksUWAnAbU8O3hIGnXjz6T
qzivyJJhFSgNGeYpwV67GfXnibpr3OZ/yx2YXIQfp0daivj++kvEU7aNfM9rHZA9
S3Gh7hKELdB9b0DkrX5GpLiZWA6NnJdrIRYbAj8CgYBCZSyJvJsxBA+EZTxOvk0Z
ZYGGCc/oUKb8p6xHVx8o35yHYQMjXWHlVaP7J03RLy3vFLnuqLvN71ixszviMQP7
2LuDCJ2YBVIVzNWgY07cgqcgQrmKZ8YCY2AOyVBdX2JD8+AVaLJmMV49r1DYBj/K
N3WlRPYJv+Ej+xmXKus+SQKBgHh/Zkthxxu+HQigL0M4teYxwSoTnj2e39uGsXBK
ICGCLIniiDVDCmswAFFkfV3G8frI+5a26t2Gqs6wIPgVVxaOlWeBROGkUNIPHMKR
iLgY8XJEg3OOfuoyql9niP5M3jyHtCOQ/Elv/YDgjUWLl0Q3KLHZLHUSl+AqvYj6
MewnAoGBANgYzPZgP+wreI55BFR470blKh1mFz+YGa+53DCd7JdMH2pdp4hoh303
XxpOSVlAuyv9SgTsZ7WjGO5UdhaBzVPKgN0OO6JQmQ5ZrOR8ZJ7VB73FiVHCEerj
1m2zyFv6OT7vqdg+V1/SzxMEmXXFQv1g69k6nWGazne3IJlzrSpj
-----END RSA PRIVATE KEY-----
12 changes: 12 additions & 0 deletions testdata/mock.jwks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"use": "sig",
"kid": "FGQdnRlzAmKyKr6-Hg_kMQrBkj_H6i6ADnBQz4OI6BU",
"alg": "RS256",
"n": "rFYQyEdjj43mnpXwj-3WgAE01TSYe1-XFE9mxUDShysFwtVZOHFSMm6kl-B3Y_O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP92ezyCEp4MPmAPFD_tY160XGrkqApuY2_-L8eEXdkRyH2H7lCYypFC0u3DIY25Vlq-ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk_NvwKOY4pJ_sm99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81wbgecd62F640scKBr3ko73L8M8UWcwgd-moKCJw"
}
]
}