Skip to content

Commit

Permalink
refactor code and cleanup as per feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
agbaraka committed Sep 10, 2024
1 parent b8487ba commit 6c999a4
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 17 deletions.
50 changes: 38 additions & 12 deletions appcheck/appcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var JWKSUrl = "https://firebaseappcheck.googleapis.com/v1beta/jwks"

const appCheckIssuer = "https://firebaseappcheck.googleapis.com/"

const tokenVerificationUrlFormat = "https://firebaseappcheck.googleapis.com/v1beta/projects/%s:verifyAppCheckToken"

var (
// ErrIncorrectAlgorithm is returned when the token is signed with a non-RSA256 algorithm.
ErrIncorrectAlgorithm = errors.New("token has incorrect algorithm")
Expand Down Expand Up @@ -70,9 +72,9 @@ type DecodedAppCheckToken struct {

// Client is the interface for the Firebase App Check service.
type Client struct {
projectID string
jwks *keyfunc.JWKS
verifyAppCheckTokenURL string
projectID string
jwks *keyfunc.JWKS
tokenVerificationUrl string
}

// NewClient creates a new instance of the Firebase App Check Client.
Expand All @@ -90,9 +92,9 @@ func NewClient(ctx context.Context, conf *internal.AppCheckConfig) (*Client, err
}

return &Client{
projectID: conf.ProjectID,
jwks: jwks,
verifyAppCheckTokenURL: fmt.Sprintf("%sv1beta/projects/%s:verifyAppCheckToken", appCheckIssuer, conf.ProjectID),
projectID: conf.ProjectID,
jwks: jwks,
tokenVerificationUrl: fmt.Sprintf(tokenVerificationUrlFormat, conf.ProjectID),
}, nil
}

Expand Down Expand Up @@ -174,10 +176,34 @@ func (c *Client) VerifyToken(token string) (*DecodedAppCheckToken, error) {
return &appCheckToken, nil
}

// VerifyTokenWithReplayProtection checks the given App Check token as follows:
// - Uses VerifyToken to validate the given token as described. if verification failed, appropriate error will be returned.
// - Checks if the token token has been consumed. if already consumed the pointer to decoded token is returned with ErrTokenAlreadyConsumed.
func (c *Client) VerifyTokenWithReplayProtection(token string) (*DecodedAppCheckToken, error) {
// VerifyOneTimeToken verifies the given App Check token and consumes it, so that it cannot be consumed again.
//
// VerifyOneTimeToken considers an App Check token string to be valid if all the following conditions are met:
// - The token string is a valid RS256 JWT.
// - The JWT contains valid issuer (iss) and audience (aud) claims that match the issuerPrefix
// and projectID of the tokenVerifier.
// - The JWT contains a valid subject (sub) claim.
// - The JWT is not expired, and it has been issued some time in the past.
// - The JWT is signed by a Firebase App Check backend server as determined by the keySource.
//
// If any of the above conditions are not met, an error is returned, regardless whether the token was
// previously consumed or not.
//
// This method currently only supports App Check tokens exchanged from the following attestation
// providers:
//
// - Play Integrity API
// - Apple App Attest
// - Apple DeviceCheck (DCDevice tokens)
// - reCAPTCHA Enterprise
// - reCAPTCHA v3
// - Custom providers
//
// App Check tokens exchanged from debug secrets are also supported. Calling this method on an
// otherwise valid App Check token with an unsupported provider will cause an error to be returned.
//
// If the token was already consumed prior to this call, an error is returned.
func (c *Client) VerifyOneTimeToken(token string) (*DecodedAppCheckToken, error) {
decodedAppCheckToken, err := c.VerifyToken(token)

if err != nil {
Expand All @@ -186,7 +212,7 @@ func (c *Client) VerifyTokenWithReplayProtection(token string) (*DecodedAppCheck

bodyReader := bytes.NewReader([]byte(fmt.Sprintf(`{"app_check_token":%s}`, token)))

resp, err := http.Post(c.verifyAppCheckTokenURL, "application/json", bodyReader)
resp, err := http.Post(c.tokenVerificationUrl, "application/json", bodyReader)

if err != nil {
return nil, err
Expand All @@ -203,7 +229,7 @@ func (c *Client) VerifyTokenWithReplayProtection(token string) (*DecodedAppCheck
}

if rb.AlreadyConsumed {
return decodedAppCheckToken, ErrTokenAlreadyConsumed
return nil, ErrTokenAlreadyConsumed
}

return decodedAppCheckToken, nil
Expand Down
10 changes: 5 additions & 5 deletions appcheck/appcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"github.com/google/go-cmp/cmp"
)

func TestVerifyTokenWithReplayProtection(t *testing.T) {
func TestVerifyOneTimeToken(t *testing.T) {

projectID := "project_id"

Expand All @@ -37,8 +37,8 @@ func TestVerifyTokenWithReplayProtection(t *testing.T) {

jwtToken := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{
Issuer: appCheckIssuer,
Audience: jwt.ClaimStrings([]string{"projects/" + projectID}),
Subject: "12345678:app:ID",
Audience: jwt.ClaimStrings([]string{"projects/12345678", "projects/" + projectID}),
Subject: "1:12345678:android:abcdef",
ExpiresAt: jwt.NewNumericDate(mockTime.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(mockTime),
NotBefore: jwt.NewNumericDate(mockTime.Add(-1 * time.Hour)),
Expand Down Expand Up @@ -79,9 +79,9 @@ func TestVerifyTokenWithReplayProtection(t *testing.T) {
t.Fatalf("error creating new client: %v", err)
}

client.verifyAppCheckTokenURL = appCheckVerifyMockServer.URL
client.tokenVerificationUrl = appCheckVerifyMockServer.URL

_, err = client.VerifyTokenWithReplayProtection(token)
_, err = client.VerifyOneTimeToken(token)

if !errors.Is(err, tt.expectedError) {
t.Errorf("failed to verify token; Expected: %v, but got: %v", tt.expectedError, err)
Expand Down

0 comments on commit 6c999a4

Please sign in to comment.