Skip to content

Commit 65444de

Browse files
sam-gcavolkovi
authored andcommitted
Add initial user object implementation (#2896)
* Initial user object implementation
1 parent bd9427b commit 65444de

File tree

8 files changed

+435
-6
lines changed

8 files changed

+435
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import { StsTokenManager, TOKEN_REFRESH_BUFFER_MS } from './token_manager';
21+
import { IdTokenResponse } from '../../model/id_token';
22+
import { createSandbox } from 'sinon';
23+
24+
use(chaiAsPromised);
25+
26+
const sandbox = createSandbox();
27+
28+
describe('core/user/token_manager', () => {
29+
let stsTokenManager: StsTokenManager;
30+
let now: number;
31+
32+
beforeEach(() => {
33+
stsTokenManager = new StsTokenManager();
34+
now = Date.now();
35+
sandbox.stub(Date, 'now').returns(now);
36+
});
37+
38+
afterEach(() => sandbox.restore());
39+
40+
describe('#isExpired', () => {
41+
it('is true if past expiration time', () => {
42+
stsTokenManager.expirationTime = 1; // Ancient history
43+
expect(stsTokenManager.isExpired).to.eq(true);
44+
});
45+
46+
it('is true if exp is in future but within buffer', () => {
47+
stsTokenManager.expirationTime = now + (TOKEN_REFRESH_BUFFER_MS - 10);
48+
expect(stsTokenManager.isExpired).to.eq(true);
49+
});
50+
51+
it('is fals if exp is far enough in future', () => {
52+
stsTokenManager.expirationTime = now + (TOKEN_REFRESH_BUFFER_MS + 10);
53+
expect(stsTokenManager.isExpired).to.eq(false);
54+
});
55+
});
56+
57+
describe('#updateFromServerResponse', () => {
58+
it('sets all the fields correctly', () => {
59+
stsTokenManager.updateFromServerResponse({
60+
idToken: 'id-token',
61+
refreshToken: 'refresh-token',
62+
expiresIn: '60' // From the server this is 30s
63+
} as IdTokenResponse);
64+
65+
expect(stsTokenManager.expirationTime).to.eq(now + 60_000);
66+
expect(stsTokenManager.accessToken).to.eq('id-token');
67+
expect(stsTokenManager.refreshToken).to.eq('refresh-token');
68+
});
69+
});
70+
71+
describe('#getToken', () => {
72+
it('throws if forceRefresh is true', async () => {
73+
Object.assign(stsTokenManager, {
74+
accessToken: 'token',
75+
expirationTime: now + 100_000
76+
});
77+
await expect(stsTokenManager.getToken(true)).to.be.rejectedWith(
78+
Error,
79+
'StsTokenManager: token refresh not implemented'
80+
);
81+
});
82+
83+
it('throws if token is expired', async () => {
84+
Object.assign(stsTokenManager, {
85+
accessToken: 'token',
86+
expirationTime: now - 1
87+
});
88+
await expect(stsTokenManager.getToken()).to.be.rejectedWith(
89+
Error,
90+
'StsTokenManager: token refresh not implemented'
91+
);
92+
});
93+
94+
it('throws if access token is missing', async () => {
95+
await expect(stsTokenManager.getToken()).to.be.rejectedWith(
96+
Error,
97+
'StsTokenManager: token refresh not implemented'
98+
);
99+
});
100+
101+
it('returns access token if not expired, not refreshing', async () => {
102+
Object.assign(stsTokenManager, {
103+
accessToken: 'token',
104+
refreshToken: 'refresh',
105+
expirationTime: now + 100_000
106+
});
107+
108+
const tokens = await stsTokenManager.getToken();
109+
expect(tokens.accessToken).to.eq('token');
110+
expect(tokens.refreshToken).to.eq('refresh');
111+
});
112+
});
113+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { IdTokenResponse } from '../../model/id_token';
19+
20+
/**
21+
* The number of milliseconds before the official expiration time of a token
22+
* to refresh that token, to provide a buffer for RPCs to complete.
23+
*/
24+
export const TOKEN_REFRESH_BUFFER_MS = 30_000;
25+
26+
export interface Tokens {
27+
accessToken: string;
28+
refreshToken: string | null;
29+
}
30+
31+
export class StsTokenManager {
32+
refreshToken: string | null = null;
33+
accessToken: string | null = null;
34+
expirationTime: number | null = null;
35+
36+
get isExpired(): boolean {
37+
return (
38+
!this.expirationTime ||
39+
Date.now() > this.expirationTime - TOKEN_REFRESH_BUFFER_MS
40+
);
41+
}
42+
43+
updateFromServerResponse({
44+
idToken,
45+
refreshToken,
46+
expiresIn: expiresInSec
47+
}: IdTokenResponse): void {
48+
this.refreshToken = refreshToken;
49+
this.accessToken = idToken;
50+
this.expirationTime = Date.now() + Number(expiresInSec) * 1000;
51+
}
52+
53+
async getToken(forceRefresh = false): Promise<Tokens> {
54+
if (!forceRefresh && this.accessToken && !this.isExpired) {
55+
return {
56+
accessToken: this.accessToken,
57+
refreshToken: this.refreshToken
58+
};
59+
}
60+
61+
throw new Error('StsTokenManager: token refresh not implemented');
62+
}
63+
64+
// TODO: There are a few more methods in here that need implemented:
65+
// # toPlainObject
66+
// # fromPlainObject
67+
// # (private) performRefresh
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import { UserImpl } from './user_impl';
21+
import { mockAuth } from '../../../test/mock_auth';
22+
import { StsTokenManager } from './token_manager';
23+
import { IdTokenResponse } from '../../model/id_token';
24+
25+
use(chaiAsPromised);
26+
27+
describe('core/user/user_impl', () => {
28+
const auth = mockAuth('foo', 'i-am-the-api-key');
29+
let stsTokenManager: StsTokenManager;
30+
31+
beforeEach(() => {
32+
stsTokenManager = new StsTokenManager();
33+
});
34+
35+
describe('constructor', () => {
36+
it('attaches required fields', () => {
37+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
38+
expect(user.auth).to.eq(auth);
39+
expect(user.uid).to.eq('uid');
40+
});
41+
42+
it('attaches optional fields if provided', () => {
43+
const user = new UserImpl({
44+
uid: 'uid',
45+
auth,
46+
stsTokenManager,
47+
displayName: 'displayName',
48+
email: 'email',
49+
phoneNumber: 'phoneNumber',
50+
photoURL: 'photoURL'
51+
});
52+
53+
expect(user.displayName).to.eq('displayName');
54+
expect(user.email).to.eq('email');
55+
expect(user.phoneNumber).to.eq('phoneNumber');
56+
expect(user.photoURL).to.eq('photoURL');
57+
});
58+
59+
it('sets optional fields to null if not provided', () => {
60+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
61+
expect(user.displayName).to.eq(null);
62+
expect(user.email).to.eq(null);
63+
expect(user.phoneNumber).to.eq(null);
64+
expect(user.photoURL).to.eq(null);
65+
});
66+
});
67+
68+
describe('#getIdToken', () => {
69+
it('returns the raw token if refresh tokens are in order', async () => {
70+
stsTokenManager.updateFromServerResponse({
71+
idToken: 'id-token-string',
72+
refreshToken: 'refresh-token-string',
73+
expiresIn: '100000'
74+
} as IdTokenResponse);
75+
76+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
77+
const token = await user.getIdToken();
78+
expect(token).to.eq('id-token-string');
79+
expect(user.refreshToken).to.eq('refresh-token-string');
80+
});
81+
82+
it('throws if refresh is required', async () => {
83+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
84+
await expect(user.getIdToken()).to.be.rejectedWith(Error);
85+
});
86+
});
87+
88+
describe('#getIdTokenResult', () => {
89+
it('throws', async () => {
90+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
91+
await expect(user.getIdTokenResult()).to.be.rejectedWith(Error);
92+
});
93+
});
94+
95+
describe('#reload', () => {
96+
it('throws', () => {
97+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
98+
expect(() => user.reload()).to.throw();
99+
});
100+
});
101+
102+
describe('#delete', () => {
103+
it('throws', () => {
104+
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
105+
expect(() => user.delete()).to.throw();
106+
});
107+
});
108+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { User } from '../../model/user';
19+
import { Auth } from '../../model/auth';
20+
import { IdTokenResult } from '../../model/id_token';
21+
import { ProviderId } from '../providers';
22+
import { StsTokenManager } from './token_manager';
23+
24+
export interface UserParameters {
25+
uid: string;
26+
auth: Auth;
27+
stsTokenManager: StsTokenManager;
28+
29+
displayName?: string;
30+
email?: string;
31+
phoneNumber?: string;
32+
photoURL?: string;
33+
}
34+
35+
export class UserImpl implements User {
36+
// For the user object, provider is always Firebase.
37+
readonly providerId = ProviderId.FIREBASE;
38+
stsTokenManager: StsTokenManager;
39+
refreshToken = '';
40+
41+
uid: string;
42+
auth: Auth;
43+
44+
// Optional fields from UserInfo
45+
displayName: string | null;
46+
email: string | null;
47+
phoneNumber: string | null;
48+
photoURL: string | null;
49+
50+
constructor({ uid, auth, stsTokenManager, ...opt }: UserParameters) {
51+
this.uid = uid;
52+
this.auth = auth;
53+
this.stsTokenManager = stsTokenManager;
54+
this.displayName = opt.displayName || null;
55+
this.email = opt.email || null;
56+
this.phoneNumber = opt.phoneNumber || null;
57+
this.photoURL = opt.photoURL || null;
58+
}
59+
60+
async getIdToken(forceRefresh?: boolean): Promise<string> {
61+
const { refreshToken, accessToken } = await this.stsTokenManager.getToken(
62+
forceRefresh
63+
);
64+
this.refreshToken = refreshToken || '';
65+
66+
// TODO: notify listeners at this point
67+
return accessToken;
68+
}
69+
70+
async getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult> {
71+
await this.getIdToken(forceRefresh);
72+
// TODO: Parse token
73+
throw new Error('Method not implemented');
74+
}
75+
76+
reload(): Promise<void> {
77+
throw new Error('Method not implemented.');
78+
}
79+
80+
delete(): Promise<void> {
81+
throw new Error('Method not implemented.');
82+
}
83+
}

0 commit comments

Comments
 (0)