Skip to content

Commit ccfe096

Browse files
Merge master into release
2 parents 12ad9f1 + 5c7fa84 commit ccfe096

File tree

23 files changed

+777
-151
lines changed

23 files changed

+777
-151
lines changed

.changeset/chilly-emus-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/app': patch
3+
---
4+
5+
App - provide a more robust check to cover more cases of empty heartbeat data.

.changeset/eight-planets-sleep.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/auth': minor
3+
'firebase': minor
4+
---
5+
6+
[feature] Add sign-in with Apple token revocation support.

.changeset/strong-coins-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/firestore-compat': patch
3+
---
4+
5+
Allow converter return value of undefined.

.changeset/tiny-items-grin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/auth': patch
3+
---
4+
5+
Fixes https://github.com/firebase/firebase-js-sdk/issues/7675

common/api-review/auth.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,9 @@ export class RecaptchaVerifier implements ApplicationVerifierInternal {
725725
// @public
726726
export function reload(user: User): Promise<void>;
727727

728+
// @public
729+
export function revokeAccessToken(auth: Auth, token: string): Promise<void>;
730+
728731
// Warning: (ae-forgotten-export) The symbol "FederatedAuthProvider" needs to be exported by the entry point index.d.ts
729732
//
730733
// @public

docs-devsite/auth.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Firebase Authentication
3535
| [isSignInWithEmailLink(auth, emailLink)](./auth.md#issigninwithemaillink) | Checks if an incoming link is a sign-in with email link suitable for [signInWithEmailLink()](./auth.md#signinwithemaillink)<!-- -->. |
3636
| [onAuthStateChanged(auth, nextOrObserver, error, completed)](./auth.md#onauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
3737
| [onIdTokenChanged(auth, nextOrObserver, error, completed)](./auth.md#onidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
38+
| [revokeAccessToken(auth, token)](./auth.md#revokeaccesstoken) | Revokes the given access token. Currently only supports Apple OAuth access tokens. |
3839
| [sendPasswordResetEmail(auth, email, actionCodeSettings)](./auth.md#sendpasswordresetemail) | Sends a password reset email to the given email address. |
3940
| [sendSignInLinkToEmail(auth, email, actionCodeSettings)](./auth.md#sendsigninlinktoemail) | Sends a sign-in email link to the user with the specified email. |
4041
| [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. |
@@ -598,6 +599,27 @@ export declare function onIdTokenChanged(auth: Auth, nextOrObserver: NextOrObser
598599

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

602+
## revokeAccessToken()
603+
604+
Revokes the given access token. Currently only supports Apple OAuth access tokens.
605+
606+
<b>Signature:</b>
607+
608+
```typescript
609+
export declare function revokeAccessToken(auth: Auth, token: string): Promise<void>;
610+
```
611+
612+
### Parameters
613+
614+
| Parameter | Type | Description |
615+
| --- | --- | --- |
616+
| auth | [Auth](./auth.auth.md#auth_interface) | The [Auth](./auth.auth.md#auth_interface) instance. |
617+
| token | string | The Apple OAuth access token. |
618+
619+
<b>Returns:</b>
620+
621+
Promise&lt;void&gt;
622+
601623
## sendPasswordResetEmail()
602624

603625
Sends a password reset email to the given email address.

packages/app/src/heartbeatService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class HeartbeatServiceImpl implements HeartbeatService {
8888
// service, not the browser user agent.
8989
const agent = platformLogger.getPlatformInfoString();
9090
const date = getUTCDateString();
91-
if (this._heartbeatsCache === null) {
91+
if (this._heartbeatsCache?.heartbeats == null) {
9292
this._heartbeatsCache = await this._heartbeatsCachePromise;
9393
}
9494
// Do not store a heartbeat if one is already stored for this day
@@ -128,7 +128,7 @@ export class HeartbeatServiceImpl implements HeartbeatService {
128128
}
129129
// If it's still null or the array is empty, there is no data to send.
130130
if (
131-
this._heartbeatsCache === null ||
131+
this._heartbeatsCache?.heartbeats == null ||
132132
this._heartbeatsCache.heartbeats.length === 0
133133
) {
134134
return '';

packages/auth/demo/src/index.js

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ import {
7373
browserPopupRedirectResolver,
7474
connectAuthEmulator,
7575
initializeRecaptchaConfig,
76-
validatePassword
76+
validatePassword,
77+
revokeAccessToken
7778
} from '@firebase/auth';
7879

7980
import { config } from './config';
@@ -1730,13 +1731,58 @@ function logAdditionalUserInfo(response) {
17301731
* Deletes the user account.
17311732
*/
17321733
function onDelete() {
1733-
activeUser()
1734-
['delete']()
1735-
.then(() => {
1736-
log('User successfully deleted.');
1737-
alertSuccess('User successfully deleted.');
1738-
refreshUserData();
1739-
}, onAuthError);
1734+
let isAppleProviderLinked = false;
1735+
1736+
for (const provider of activeUser().providerData) {
1737+
if (provider.providerId == 'apple.com') {
1738+
isAppleProviderLinked = true;
1739+
break;
1740+
}
1741+
}
1742+
1743+
if (isAppleProviderLinked) {
1744+
revokeAppleTokenAndDeleteUser();
1745+
} else {
1746+
activeUser()
1747+
['delete']()
1748+
.then(() => {
1749+
log('User successfully deleted.');
1750+
alertSuccess('User successfully deleted.');
1751+
refreshUserData();
1752+
}, onAuthError);
1753+
}
1754+
}
1755+
1756+
function revokeAppleTokenAndDeleteUser() {
1757+
// Re-auth then revoke the token
1758+
const provider = new OAuthProvider('apple.com');
1759+
provider.addScope('email');
1760+
provider.addScope('name');
1761+
1762+
const auth = getAuth();
1763+
signInWithPopup(auth, provider).then(result => {
1764+
// The signed-in user info.
1765+
const credential = OAuthProvider.credentialFromResult(result);
1766+
const accessToken = credential.accessToken;
1767+
1768+
revokeAccessToken(auth, accessToken)
1769+
.then(() => {
1770+
log('Token successfully revoked.');
1771+
1772+
// Usual user deletion
1773+
activeUser()
1774+
['delete']()
1775+
.then(() => {
1776+
log('User successfully deleted.');
1777+
alertSuccess('User successfully deleted.');
1778+
refreshUserData();
1779+
}, onAuthError);
1780+
})
1781+
.catch(error => {
1782+
log('Failed to revoke token. ', error.message);
1783+
alertError('Failed to revoke token. ', error.message);
1784+
});
1785+
});
17401786
}
17411787

17421788
/**

packages/auth/src/api/account_management/email_and_password.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as mockFetch from '../../../test/helpers/mock_fetch';
2727
import { ServerError } from '../errors';
2828
import {
2929
applyActionCode,
30+
linkEmailPassword,
3031
resetPassword,
3132
updateEmailPassword
3233
} from './email_and_password';
@@ -91,6 +92,65 @@ describe('api/account_management/resetPassword', () => {
9192
});
9293
});
9394

95+
describe('api/account_management/linkEmailPassword', () => {
96+
const request = {
97+
idToken: 'id-token',
98+
returnSecureToken: true,
99+
100+
password: 'new-password'
101+
};
102+
103+
let auth: TestAuth;
104+
105+
beforeEach(async () => {
106+
auth = await testAuth();
107+
mockFetch.setUp();
108+
});
109+
110+
afterEach(mockFetch.tearDown);
111+
112+
it('should POST to the correct endpoint', async () => {
113+
const mock = mockEndpoint(Endpoint.SIGN_UP, {
114+
idToken: 'id-token'
115+
});
116+
117+
const response = await linkEmailPassword(auth, request);
118+
expect(response.idToken).to.eq('id-token');
119+
expect(mock.calls[0].request).to.eql(request);
120+
expect(mock.calls[0].method).to.eq('POST');
121+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
122+
'application/json'
123+
);
124+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
125+
'testSDK/0.0.0'
126+
);
127+
});
128+
129+
it('should handle errors', async () => {
130+
const mock = mockEndpoint(
131+
Endpoint.SIGN_UP,
132+
{
133+
error: {
134+
code: 400,
135+
message: ServerError.INVALID_EMAIL,
136+
errors: [
137+
{
138+
message: ServerError.INVALID_EMAIL
139+
}
140+
]
141+
}
142+
},
143+
400
144+
);
145+
146+
await expect(linkEmailPassword(auth, request)).to.be.rejectedWith(
147+
FirebaseError,
148+
'Firebase: The email address is badly formatted. (auth/invalid-email).'
149+
);
150+
expect(mock.calls[0].request).to.eql(request);
151+
});
152+
});
153+
94154
describe('api/account_management/updateEmailPassword', () => {
95155
const request = {
96156
idToken: 'id-token',

packages/auth/src/api/account_management/email_and_password.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '../index';
2626
import { IdTokenResponse } from '../../model/id_token';
2727
import { MfaEnrollment } from './mfa';
28+
import { SignUpRequest, SignUpResponse } from '../authentication/sign_up';
2829

2930
export interface ResetPasswordRequest {
3031
oobCode: string;
@@ -69,6 +70,20 @@ export async function updateEmailPassword(
6970
>(auth, HttpMethod.POST, Endpoint.SET_ACCOUNT_INFO, request);
7071
}
7172

73+
// Used for linking an email/password account to an existing idToken. Uses the same request/response
74+
// format as updateEmailPassword.
75+
export async function linkEmailPassword(
76+
auth: Auth,
77+
request: SignUpRequest
78+
): Promise<SignUpResponse> {
79+
return _performApiRequest<SignUpRequest, SignUpResponse>(
80+
auth,
81+
HttpMethod.POST,
82+
Endpoint.SIGN_UP,
83+
request
84+
);
85+
}
86+
7287
export interface ApplyActionCodeRequest {
7388
oobCode: string;
7489
tenantId?: string;

packages/auth/src/api/authentication/sign_up.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IdTokenResponse } from '../../model/id_token';
2727
import { Auth } from '../../model/public_types';
2828

2929
export interface SignUpRequest {
30+
idToken?: string;
3031
returnSecureToken?: boolean;
3132
email?: string;
3233
password?: string;

packages/auth/src/api/authentication/token.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import chaiAsPromised from 'chai-as-promised';
2121

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

24-
import { HttpHeader } from '../';
24+
import { Endpoint, HttpHeader } from '../';
25+
import { mockEndpoint } from '../../../test/helpers/api/helper';
2526
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
2627
import * as fetch from '../../../test/helpers/mock_fetch';
2728
import { ServerError } from '../errors';
28-
import { Endpoint, requestStsToken } from './token';
29+
import { TokenType, requestStsToken, revokeToken } from './token';
2930
import { SDK_VERSION } from '@firebase/app';
3031
import { _getBrowserName } from '../../core/util/browser';
3132

@@ -143,3 +144,63 @@ describe('requestStsToken', () => {
143144
});
144145
});
145146
});
147+
148+
describe('api/authentication/revokeToken', () => {
149+
const request = {
150+
providerId: 'provider-id',
151+
tokenType: TokenType.ACCESS_TOKEN,
152+
token: 'token',
153+
idToken: 'id-token'
154+
};
155+
156+
let auth: TestAuth;
157+
158+
beforeEach(async () => {
159+
auth = await testAuth();
160+
fetch.setUp();
161+
});
162+
163+
afterEach(() => {
164+
fetch.tearDown();
165+
});
166+
167+
it('should POST to the correct endpoint', async () => {
168+
const mock = mockEndpoint(Endpoint.REVOKE_TOKEN, {});
169+
170+
auth.tenantId = 'tenant-id';
171+
await revokeToken(auth, request);
172+
// Currently, backend returns an empty response.
173+
expect(mock.calls[0].request).to.eql({ ...request, tenantId: 'tenant-id' });
174+
expect(mock.calls[0].method).to.eq('POST');
175+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
176+
'application/json'
177+
);
178+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
179+
'testSDK/0.0.0'
180+
);
181+
});
182+
183+
it('should handle errors', async () => {
184+
const mock = mockEndpoint(
185+
Endpoint.REVOKE_TOKEN,
186+
{
187+
error: {
188+
code: 400,
189+
message: ServerError.INVALID_IDP_RESPONSE,
190+
errors: [
191+
{
192+
message: ServerError.INVALID_IDP_RESPONSE
193+
}
194+
]
195+
}
196+
},
197+
400
198+
);
199+
200+
await expect(revokeToken(auth, request)).to.be.rejectedWith(
201+
FirebaseError,
202+
'Firebase: The supplied auth credential is malformed or has expired. (auth/invalid-credential).'
203+
);
204+
expect(mock.calls[0].request).to.eql(request);
205+
});
206+
});

0 commit comments

Comments
 (0)