|  | 
|  | 1 | +// Copyright 2022 Google Inc. All Rights Reserved. | 
|  | 2 | +// | 
|  | 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 4 | +// you may not use this file except in compliance with the License. | 
|  | 5 | +// You may obtain a copy of the License at | 
|  | 6 | +// | 
|  | 7 | +//      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 8 | +// | 
|  | 9 | +// Unless required by applicable law or agreed to in writing, software | 
|  | 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 12 | +// See the License for the specific language governing permissions and | 
|  | 13 | +// limitations under the License. | 
|  | 14 | + | 
|  | 15 | +// Package appcheck provides functionality for verifying App Check tokens. | 
|  | 16 | +package appcheck | 
|  | 17 | + | 
|  | 18 | +import ( | 
|  | 19 | +	"context" | 
|  | 20 | +	"errors" | 
|  | 21 | +	"strings" | 
|  | 22 | +	"time" | 
|  | 23 | + | 
|  | 24 | +	"github.com/MicahParks/keyfunc" | 
|  | 25 | +	"github.com/golang-jwt/jwt/v4" | 
|  | 26 | + | 
|  | 27 | +	"firebase.google.com/go/v4/internal" | 
|  | 28 | +) | 
|  | 29 | + | 
|  | 30 | +// JWKSUrl is the URL of the JWKS used to verify App Check tokens. | 
|  | 31 | +var JWKSUrl = "https://firebaseappcheck.googleapis.com/v1beta/jwks" | 
|  | 32 | + | 
|  | 33 | +const appCheckIssuer = "https://firebaseappcheck.googleapis.com/" | 
|  | 34 | + | 
|  | 35 | +var ( | 
|  | 36 | +	// ErrIncorrectAlgorithm is returned when the token is signed with a non-RSA256 algorithm. | 
|  | 37 | +	ErrIncorrectAlgorithm = errors.New("token has incorrect algorithm") | 
|  | 38 | +	// ErrTokenType is returned when the token is not a JWT. | 
|  | 39 | +	ErrTokenType = errors.New("token has incorrect type") | 
|  | 40 | +	// ErrTokenClaims is returned when the token claims cannot be decoded. | 
|  | 41 | +	ErrTokenClaims = errors.New("token has incorrect claims") | 
|  | 42 | +	// ErrTokenAudience is returned when the token audience does not match the current project. | 
|  | 43 | +	ErrTokenAudience = errors.New("token has incorrect audience") | 
|  | 44 | +	// ErrTokenIssuer is returned when the token issuer does not match Firebase's App Check service. | 
|  | 45 | +	ErrTokenIssuer = errors.New("token has incorrect issuer") | 
|  | 46 | +	// ErrTokenSubject is returned when the token subject is empty or missing. | 
|  | 47 | +	ErrTokenSubject = errors.New("token has empty or missing subject") | 
|  | 48 | +) | 
|  | 49 | + | 
|  | 50 | +// DecodedAppCheckToken represents a verified App Check token. | 
|  | 51 | +// | 
|  | 52 | +// DecodedAppCheckToken provides typed accessors to the common JWT fields such as Audience (aud) | 
|  | 53 | +// and ExpiresAt (exp). Additionally it provides an AppID field, which indicates the application ID to which this | 
|  | 54 | +// token belongs. Any additional JWT claims can be accessed via the Claims map of DecodedAppCheckToken. | 
|  | 55 | +type DecodedAppCheckToken struct { | 
|  | 56 | +	Issuer    string | 
|  | 57 | +	Subject   string | 
|  | 58 | +	Audience  []string | 
|  | 59 | +	ExpiresAt time.Time | 
|  | 60 | +	IssuedAt  time.Time | 
|  | 61 | +	AppID     string | 
|  | 62 | +	Claims    map[string]interface{} | 
|  | 63 | +} | 
|  | 64 | + | 
|  | 65 | +// Client is the interface for the Firebase App Check service. | 
|  | 66 | +type Client struct { | 
|  | 67 | +	projectID string | 
|  | 68 | +	jwks      *keyfunc.JWKS | 
|  | 69 | +} | 
|  | 70 | + | 
|  | 71 | +// NewClient creates a new instance of the Firebase App Check Client. | 
|  | 72 | +// | 
|  | 73 | +// This function can only be invoked from within the SDK. Client applications should access the | 
|  | 74 | +// the App Check service through firebase.App. | 
|  | 75 | +func NewClient(ctx context.Context, conf *internal.AppCheckConfig) (*Client, error) { | 
|  | 76 | +	// TODO: Add support for overriding the HTTP client using the App one. | 
|  | 77 | +	jwks, err := keyfunc.Get(JWKSUrl, keyfunc.Options{ | 
|  | 78 | +		Ctx:             ctx, | 
|  | 79 | +		RefreshInterval: 6 * time.Hour, | 
|  | 80 | +	}) | 
|  | 81 | +	if err != nil { | 
|  | 82 | +		return nil, err | 
|  | 83 | +	} | 
|  | 84 | + | 
|  | 85 | +	return &Client{ | 
|  | 86 | +		projectID: conf.ProjectID, | 
|  | 87 | +		jwks:      jwks, | 
|  | 88 | +	}, nil | 
|  | 89 | +} | 
|  | 90 | + | 
|  | 91 | +// VerifyToken verifies the given App Check token. | 
|  | 92 | +// | 
|  | 93 | +// VerifyToken considers an App Check token string to be valid if all the following conditions are met: | 
|  | 94 | +//   - The token string is a valid RS256 JWT. | 
|  | 95 | +//   - The JWT contains valid issuer (iss) and audience (aud) claims that match the issuerPrefix | 
|  | 96 | +//     and projectID of the tokenVerifier. | 
|  | 97 | +//   - The JWT contains a valid subject (sub) claim. | 
|  | 98 | +//   - The JWT is not expired, and it has been issued some time in the past. | 
|  | 99 | +//   - The JWT is signed by a Firebase App Check backend server as determined by the keySource. | 
|  | 100 | +// | 
|  | 101 | +// If any of the above conditions are not met, an error is returned. Otherwise a pointer to a | 
|  | 102 | +// decoded App Check token is returned. | 
|  | 103 | +func (c *Client) VerifyToken(token string) (*DecodedAppCheckToken, error) { | 
|  | 104 | +	// References for checks: | 
|  | 105 | +	// https://firebase.googleblog.com/2021/10/protecting-backends-with-app-check.html | 
|  | 106 | +	// https://github.com/firebase/firebase-admin-node/blob/master/src/app-check/token-verifier.ts#L106 | 
|  | 107 | + | 
|  | 108 | +	// The standard JWT parser also validates the expiration of the token | 
|  | 109 | +	// so we do not need dedicated code for that. | 
|  | 110 | +	decodedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { | 
|  | 111 | +		if t.Header["alg"] != "RS256" { | 
|  | 112 | +			return nil, ErrIncorrectAlgorithm | 
|  | 113 | +		} | 
|  | 114 | +		if t.Header["typ"] != "JWT" { | 
|  | 115 | +			return nil, ErrTokenType | 
|  | 116 | +		} | 
|  | 117 | +		return c.jwks.Keyfunc(t) | 
|  | 118 | +	}) | 
|  | 119 | +	if err != nil { | 
|  | 120 | +		return nil, err | 
|  | 121 | +	} | 
|  | 122 | + | 
|  | 123 | +	claims, ok := decodedToken.Claims.(jwt.MapClaims) | 
|  | 124 | +	if !ok { | 
|  | 125 | +		return nil, ErrTokenClaims | 
|  | 126 | +	} | 
|  | 127 | + | 
|  | 128 | +	rawAud := claims["aud"].([]interface{}) | 
|  | 129 | +	aud := []string{} | 
|  | 130 | +	for _, v := range rawAud { | 
|  | 131 | +		aud = append(aud, v.(string)) | 
|  | 132 | +	} | 
|  | 133 | + | 
|  | 134 | +	if !contains(aud, "projects/"+c.projectID) { | 
|  | 135 | +		return nil, ErrTokenAudience | 
|  | 136 | +	} | 
|  | 137 | + | 
|  | 138 | +	// We check the prefix to make sure this token was issued | 
|  | 139 | +	// by the Firebase App Check service, but we do not check the | 
|  | 140 | +	// Project Number suffix because the Golang SDK only has project ID. | 
|  | 141 | +	// | 
|  | 142 | +	// This is consistent with the Firebase Admin Node SDK. | 
|  | 143 | +	if !strings.HasPrefix(claims["iss"].(string), appCheckIssuer) { | 
|  | 144 | +		return nil, ErrTokenIssuer | 
|  | 145 | +	} | 
|  | 146 | + | 
|  | 147 | +	if val, ok := claims["sub"].(string); !ok || val == "" { | 
|  | 148 | +		return nil, ErrTokenSubject | 
|  | 149 | +	} | 
|  | 150 | + | 
|  | 151 | +	appCheckToken := DecodedAppCheckToken{ | 
|  | 152 | +		Issuer:    claims["iss"].(string), | 
|  | 153 | +		Subject:   claims["sub"].(string), | 
|  | 154 | +		Audience:  aud, | 
|  | 155 | +		ExpiresAt: time.Unix(int64(claims["exp"].(float64)), 0), | 
|  | 156 | +		IssuedAt:  time.Unix(int64(claims["iat"].(float64)), 0), | 
|  | 157 | +		AppID:     claims["sub"].(string), | 
|  | 158 | +	} | 
|  | 159 | + | 
|  | 160 | +	// Remove all the claims we've already parsed. | 
|  | 161 | +	for _, usedClaim := range []string{"iss", "sub", "aud", "exp", "iat", "sub"} { | 
|  | 162 | +		delete(claims, usedClaim) | 
|  | 163 | +	} | 
|  | 164 | +	appCheckToken.Claims = claims | 
|  | 165 | + | 
|  | 166 | +	return &appCheckToken, nil | 
|  | 167 | +} | 
|  | 168 | + | 
|  | 169 | +func contains(s []string, str string) bool { | 
|  | 170 | +	for _, v := range s { | 
|  | 171 | +		if v == str { | 
|  | 172 | +			return true | 
|  | 173 | +		} | 
|  | 174 | +	} | 
|  | 175 | +	return false | 
|  | 176 | +} | 
0 commit comments