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

Add replay protection feature #641

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
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
49 changes: 39 additions & 10 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"
agbaraka marked this conversation as resolved.
Show resolved Hide resolved

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,16 +176,43 @@ func (c *Client) VerifyToken(token string) (*DecodedAppCheckToken, error) {
return &appCheckToken, nil
}

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 {
return nil, fmt.Errorf("failed to verify token: %v", err)
return nil, err
}

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 @@ -200,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