Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/eight-planets-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/auth': minor
'firebase': minor
---

[feature] Add sign-in with Apple token revocation support.
3 changes: 3 additions & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,9 @@ export class RecaptchaVerifier implements ApplicationVerifierInternal {
// @public
export function reload(user: User): Promise<void>;

// @public
export function revokeAccessToken(auth: Auth, token: string): Promise<void>;

// Warning: (ae-forgotten-export) The symbol "FederatedAuthProvider" needs to be exported by the entry point index.d.ts
//
// @public
Expand Down
22 changes: 22 additions & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Firebase Authentication
| [isSignInWithEmailLink(auth, emailLink)](./auth.md#issigninwithemaillink) | Checks if an incoming link is a sign-in with email link suitable for [signInWithEmailLink()](./auth.md#signinwithemaillink)<!-- -->. |
| [onAuthStateChanged(auth, nextOrObserver, error, completed)](./auth.md#onauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
| [onIdTokenChanged(auth, nextOrObserver, error, completed)](./auth.md#onidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
| [revokeAccessToken(auth, token)](./auth.md#revokeaccesstoken) | Revokes the given access token. Currently only supports Apple OAuth access tokens. |
| [sendPasswordResetEmail(auth, email, actionCodeSettings)](./auth.md#sendpasswordresetemail) | Sends a password reset email to the given email address. |
| [sendSignInLinkToEmail(auth, email, actionCodeSettings)](./auth.md#sendsigninlinktoemail) | Sends a sign-in email link to the user with the specified email. |
| [setPersistence(auth, persistence)](./auth.md#setpersistence) | Changes the type of persistence on the [Auth](./auth.auth.md#auth_interface) instance for the currently saved <code>Auth</code> session and applies this type of persistence for future sign-in requests, including sign-in with redirect requests. |
Expand Down Expand Up @@ -598,6 +599,27 @@ export declare function onIdTokenChanged(auth: Auth, nextOrObserver: NextOrObser

[Unsubscribe](./util.md#unsubscribe)

## revokeAccessToken()

Revokes the given access token. Currently only supports Apple OAuth access tokens.

<b>Signature:</b>

```typescript
export declare function revokeAccessToken(auth: Auth, token: string): Promise<void>;
```

### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| auth | [Auth](./auth.auth.md#auth_interface) | The [Auth](./auth.auth.md#auth_interface) instance. |
| token | string | The Apple OAuth access token. |

<b>Returns:</b>

Promise&lt;void&gt;

## sendPasswordResetEmail()

Sends a password reset email to the given email address.
Expand Down
62 changes: 54 additions & 8 deletions packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ import {
browserPopupRedirectResolver,
connectAuthEmulator,
initializeRecaptchaConfig,
validatePassword
validatePassword,
revokeAccessToken
} from '@firebase/auth';

import { config } from './config';
Expand Down Expand Up @@ -1730,13 +1731,58 @@ function logAdditionalUserInfo(response) {
* Deletes the user account.
*/
function onDelete() {
activeUser()
['delete']()
.then(() => {
log('User successfully deleted.');
alertSuccess('User successfully deleted.');
refreshUserData();
}, onAuthError);
let isAppleProviderLinked = false;

for (const provider of activeUser().providerData) {
if (provider.providerId == 'apple.com') {
isAppleProviderLinked = true;
break;
}
}

if (isAppleProviderLinked) {
revokeAppleTokenAndDeleteUser();
} else {
activeUser()
['delete']()
.then(() => {
log('User successfully deleted.');
alertSuccess('User successfully deleted.');
refreshUserData();
}, onAuthError);
}
}

function revokeAppleTokenAndDeleteUser() {
// Re-auth then revoke the token
const provider = new OAuthProvider('apple.com');
provider.addScope('email');
provider.addScope('name');

const auth = getAuth();
signInWithPopup(auth, provider).then(result => {
// The signed-in user info.
const credential = OAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;

revokeAccessToken(auth, accessToken)
.then(() => {
log('Token successfully revoked.');

// Usual user deletion
activeUser()
['delete']()
.then(() => {
log('User successfully deleted.');
alertSuccess('User successfully deleted.');
refreshUserData();
}, onAuthError);
})
.catch(error => {
log('Failed to revoke token. ', error.message);
alertError('Failed to revoke token. ', error.message);
});
});
}

/**
Expand Down
65 changes: 63 additions & 2 deletions packages/auth/src/api/authentication/token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import chaiAsPromised from 'chai-as-promised';

import { FirebaseError, getUA, querystringDecode } from '@firebase/util';

import { HttpHeader } from '../';
import { Endpoint, HttpHeader } from '../';
import { mockEndpoint } from '../../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as fetch from '../../../test/helpers/mock_fetch';
import { ServerError } from '../errors';
import { Endpoint, requestStsToken } from './token';
import { TokenType, requestStsToken, revokeToken } from './token';
import { SDK_VERSION } from '@firebase/app';
import { _getBrowserName } from '../../core/util/browser';

Expand Down Expand Up @@ -143,3 +144,63 @@ describe('requestStsToken', () => {
});
});
});

describe('api/authentication/revokeToken', () => {
const request = {
providerId: 'provider-id',
tokenType: TokenType.ACCESS_TOKEN,
token: 'token',
idToken: 'id-token'
};

let auth: TestAuth;

beforeEach(async () => {
auth = await testAuth();
fetch.setUp();
});

afterEach(() => {
fetch.tearDown();
});

it('should POST to the correct endpoint', async () => {
const mock = mockEndpoint(Endpoint.REVOKE_TOKEN, {});

auth.tenantId = 'tenant-id';
await revokeToken(auth, request);
// Currently, backend returns an empty response.
expect(mock.calls[0].request).to.eql({ ...request, tenantId: 'tenant-id' });
expect(mock.calls[0].method).to.eq('POST');
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
'application/json'
);
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
'testSDK/0.0.0'
);
});

it('should handle errors', async () => {
const mock = mockEndpoint(
Endpoint.REVOKE_TOKEN,
{
error: {
code: 400,
message: ServerError.INVALID_IDP_RESPONSE,
errors: [
{
message: ServerError.INVALID_IDP_RESPONSE
}
]
}
},
400
);

await expect(revokeToken(auth, request)).to.be.rejectedWith(
FirebaseError,
'Firebase: The supplied auth credential is malformed or has expired. (auth/invalid-credential).'
);
expect(mock.calls[0].request).to.eql(request);
});
});
32 changes: 29 additions & 3 deletions packages/auth/src/api/authentication/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ import { querystring } from '@firebase/util';
import {
_getFinalTarget,
_performFetchWithErrorHandling,
_performApiRequest,
_addTidIfNecessary,
HttpMethod,
HttpHeader
HttpHeader,
Endpoint
} from '../index';
import { FetchProvider } from '../../core/util/fetch_provider';
import { Auth } from '../../model/public_types';
import { AuthInternal } from '../../model/auth';

export const enum Endpoint {
TOKEN = '/v1/token'
export const enum TokenType {
REFRESH_TOKEN = 'REFRESH_TOKEN',
ACCESS_TOKEN = 'ACCESS_TOKEN'
}

/** The server responses with snake_case; we convert to camelCase */
Expand All @@ -46,6 +50,16 @@ export interface RequestStsTokenResponse {
refreshToken: string;
}

export interface RevokeTokenRequest {
providerId: string;
tokenType: TokenType;
token: string;
idToken: string;
tenantId?: string;
}

export interface RevokeTokenResponse {}

export async function requestStsToken(
auth: Auth,
refreshToken: string
Expand Down Expand Up @@ -85,3 +99,15 @@ export async function requestStsToken(
refreshToken: response.refresh_token
};
}

export async function revokeToken(
auth: Auth,
request: RevokeTokenRequest
): Promise<RevokeTokenResponse> {
return _performApiRequest<RevokeTokenRequest, RevokeTokenResponse>(
auth,
HttpMethod.POST,
Endpoint.REVOKE_TOKEN,
_addTidIfNecessary(auth, request)
);
}
4 changes: 3 additions & 1 deletion packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export const enum Endpoint {
WITHDRAW_MFA = '/v2/accounts/mfaEnrollment:withdraw',
GET_PROJECT_CONFIG = '/v1/projects',
GET_RECAPTCHA_CONFIG = '/v2/recaptchaConfig',
GET_PASSWORD_POLICY = '/v2/passwordPolicy'
GET_PASSWORD_POLICY = '/v2/passwordPolicy',
TOKEN = '/v1/token',
REVOKE_TOKEN = '/v2/accounts:revokeToken'
}

export const enum RecaptchaClientType {
Expand Down
25 changes: 25 additions & 0 deletions packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ import { _getInstance } from '../util/instantiator';
import { _getUserLanguage } from '../util/navigator';
import { _getClientVersion } from '../util/version';
import { HttpHeader } from '../../api';
import {
RevokeTokenRequest,
TokenType,
revokeToken
} from '../../api/authentication/token';
import { AuthMiddlewareQueue } from './middleware';
import { RecaptchaConfig } from '../../platform_browser/recaptcha/recaptcha';
import { _logWarn } from '../util/log';
Expand Down Expand Up @@ -514,6 +519,26 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
});
}

/**
* Revokes the given access token. Currently only supports Apple OAuth access tokens.
*/
async revokeAccessToken(token: string): Promise<void> {
if (this.currentUser) {
const idToken = await this.currentUser.getIdToken();
// Generalize this to accept other providers once supported.
const request: RevokeTokenRequest = {
providerId: 'apple.com',
tokenType: TokenType.ACCESS_TOKEN,
token,
idToken
};
if (this.tenantId != null) {
request.tenantId = this.tenantId;
}
await revokeToken(this, request);
}
}

toJSON(): object {
return {
apiKey: this.config.apiKey,
Expand Down
13 changes: 13 additions & 0 deletions packages/auth/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,19 @@ export function signOut(auth: Auth): Promise<void> {
return getModularInstance(auth).signOut();
}

/**
* Revokes the given access token. Currently only supports Apple OAuth access tokens.
*
* @param auth - The {@link Auth} instance.
* @param token - The Apple OAuth access token.
*
* @public
*/
export function revokeAccessToken(auth: Auth, token: string): Promise<void> {
const authInternal = _castAuth(auth);
return authInternal.revokeAccessToken(token);
}

export { initializeAuth } from './auth/initialize';
export { connectAuthEmulator } from './auth/emulator';

Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/core/user/token_manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import { FirebaseError } from '@firebase/util';

import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as fetch from '../../../test/helpers/mock_fetch';
import { Endpoint } from '../../api/authentication/token';
import { IdTokenResponse } from '../../model/id_token';
import { StsTokenManager, Buffer } from './token_manager';
import { FinalizeMfaResponse } from '../../api/authentication/mfa';
import { makeJWT } from '../../../test/helpers/jwt';
import { Endpoint } from '../../api';

use(chaiAsPromised);

Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/model/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ export interface AuthInternal extends Auth {
useDeviceLanguage(): void;
signOut(): Promise<void>;
validatePassword(password: string): Promise<PasswordValidationStatus>;
revokeAccessToken(token: string): Promise<void>;
}