diff --git a/auth/auth.go b/auth/auth.go index a7028698..b615d5f6 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -18,8 +18,11 @@ package auth import ( "context" + "encoding/json" "errors" "fmt" + "net/http" + "net/url" "os" "strings" "time" @@ -146,23 +149,25 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error) } idToolkitV1Endpoint := fmt.Sprintf("%s/v1", baseURL) idToolkitV2Endpoint := fmt.Sprintf("%s/v2", baseURL) + secureToolkitV1Endpoint := fmt.Sprintf("%s/v1", "https://securetoken.googleapis.com") userManagementEndpoint := idToolkitV1Endpoint providerConfigEndpoint := idToolkitV2Endpoint tenantMgtEndpoint := idToolkitV2Endpoint projectMgtEndpoint := idToolkitV2Endpoint base := &baseClient{ - userManagementEndpoint: userManagementEndpoint, - providerConfigEndpoint: providerConfigEndpoint, - tenantMgtEndpoint: tenantMgtEndpoint, - projectMgtEndpoint: projectMgtEndpoint, - projectID: conf.ProjectID, - httpClient: hc, - idTokenVerifier: idTokenVerifier, - cookieVerifier: cookieVerifier, - signer: signer, - clock: internal.SystemClock, - isEmulator: isEmulator, + secureToolkitV1Endpoint: secureToolkitV1Endpoint, // here + userManagementEndpoint: userManagementEndpoint, + providerConfigEndpoint: providerConfigEndpoint, + tenantMgtEndpoint: tenantMgtEndpoint, + projectMgtEndpoint: projectMgtEndpoint, + projectID: conf.ProjectID, + httpClient: hc, + idTokenVerifier: idTokenVerifier, + cookieVerifier: cookieVerifier, + signer: signer, + clock: internal.SystemClock, + isEmulator: isEmulator, } return &Client{ baseClient: base, @@ -170,6 +175,57 @@ func NewClient(ctx context.Context, conf *internal.AuthConfig) (*Client, error) }, nil } +type TokenResponse struct { + ExpiresIn string `json:"expires_in"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + UserID string `json:"user_id"` + ProjectID string `json:"project_id"` +} + +func fetchIdTokenByRefreshToken(ctx context.Context, apiKey, refreshToken, endpointBase string) (TokenResponse, error) { + endpoint := fmt.Sprintf("%s/token?key=%s", endpointBase, apiKey) + + form := url.Values{} + form.Add("grant_type", "refresh_token") + form.Add("refresh_token", refreshToken) + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(form.Encode())) + if err != nil { + return TokenResponse{}, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return TokenResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return TokenResponse{}, fmt.Errorf("fetchIdTokenByRefreshToken: %s", resp.Status) + } + + var tokenResp TokenResponse + err = json.NewDecoder(resp.Body).Decode(&tokenResp) + if err != nil { + return TokenResponse{}, err + } + + return tokenResp, nil +} + +func (c *baseClient) ExchangeIdToken(ctx context.Context, apikey string, refreshToken string) (string, error) { + res, err := fetchIdTokenByRefreshToken(ctx, apikey, refreshToken, c.secureToolkitV1Endpoint) + if err != nil { + return "", err + } + return res.IDToken, nil +} + // CustomToken creates a signed custom authentication token with the specified user ID. // // The resulting JWT can be used in a Firebase client SDK to trigger an authentication flow. See @@ -274,18 +330,19 @@ type FirebaseInfo struct { // baseClient exposes the APIs common to both auth.Client and auth.TenantClient. type baseClient struct { - userManagementEndpoint string - providerConfigEndpoint string - tenantMgtEndpoint string - projectMgtEndpoint string - projectID string - tenantID string - httpClient *internal.HTTPClient - idTokenVerifier *tokenVerifier - cookieVerifier *tokenVerifier - signer cryptoSigner - clock internal.Clock - isEmulator bool + secureToolkitV1Endpoint string + userManagementEndpoint string + providerConfigEndpoint string + tenantMgtEndpoint string + projectMgtEndpoint string + projectID string + tenantID string + httpClient *internal.HTTPClient + idTokenVerifier *tokenVerifier + cookieVerifier *tokenVerifier + signer cryptoSigner + clock internal.Clock + isEmulator bool } func (c *baseClient) withTenantID(tenantID string) *baseClient { diff --git a/integration/auth/auth_test.go b/integration/auth/auth_test.go index 66809972..08fa5e85 100644 --- a/integration/auth/auth_test.go +++ b/integration/auth/auth_test.go @@ -135,6 +135,48 @@ func TestCustomTokenWithClaims(t *testing.T) { } } +func TestExchangeIdToken(t *testing.T) { + uid := "fetch_id_token_by_refresh_token" + ct, err := client.CustomToken(context.Background(), uid) + if err != nil { + t.Fatal(err) + } + idt, err := signInWithCustomToken(ct) + if err != nil { + t.Fatal(err) + } + defer deleteUser(uid) + + vt, err := client.VerifyIDTokenAndCheckRevoked(context.Background(), idt) + if err != nil { + t.Fatal(err) + } + if vt.UID != uid { + t.Errorf("UID = %q; want UID = %q", vt.UID, uid) + } + + apiKey, err := internal.APIKey() + if err != nil { + t.Errorf("internal.APIKey() = %v; want = ", err) + } + + time.Sleep(time.Second) + newIdt, err := client.ExchangeIdToken(context.Background(), apiKey, "mock-refresh-token") + if err != nil { + t.Fatal(err) + } + + if idt == newIdt { + // The new ID token should be different from the old one. + t.Errorf("ID token = %q; want ID token != %q", newIdt, idt) + } + + _, err = client.VerifyIDTokenAndCheckRevoked(context.Background(), newIdt) + if err != nil { + t.Errorf("VerifyIDTokenAndCheckRevoked(); err = %s; want err = ", err) + } +} + func TestRevokeRefreshTokens(t *testing.T) { uid := "user_revoked" ct, err := client.CustomToken(context.Background(), uid)