Skip to content

Commit 1de07ba

Browse files
authored
[Auth] Add SAML support to the new SDK (#4619)
* Initial saml support * Round out saml support w/ tests; break out oauth providers * Formatting, license * Fix tests * Formatting
1 parent 8dab8e1 commit 1de07ba

File tree

15 files changed

+599
-102
lines changed

15 files changed

+599
-102
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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 } from 'chai';
19+
20+
import { mockEndpoint } from '../../../test/helpers/api/helper';
21+
import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response';
22+
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
23+
import * as fetch from '../../../test/helpers/mock_fetch';
24+
import { Endpoint } from '../../api';
25+
import { SignInWithIdpRequest } from '../../api/authentication/idp';
26+
import { SAMLAuthCredential } from './saml';
27+
28+
describe('core/credentials/saml', () => {
29+
let auth: TestAuth;
30+
let signInWithIdp: fetch.Route;
31+
32+
beforeEach(async () => {
33+
auth = await testAuth();
34+
fetch.setUp();
35+
36+
signInWithIdp = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, {
37+
...TEST_ID_TOKEN_RESPONSE
38+
});
39+
});
40+
41+
afterEach(() => {
42+
fetch.tearDown();
43+
});
44+
45+
context('_create', () => {
46+
it('sets the provider', () => {
47+
const cred = SAMLAuthCredential._create('saml.provider', 'pending-token');
48+
expect(cred.providerId).to.eq('saml.provider');
49+
});
50+
});
51+
52+
context('#toJSON', () => {
53+
it('packs up everything', () => {
54+
const cred = SAMLAuthCredential._create('saml.provider', 'pending-token');
55+
56+
expect(cred.toJSON()).to.eql({
57+
signInMethod: 'saml.provider',
58+
providerId: 'saml.provider',
59+
pendingToken: 'pending-token'
60+
});
61+
});
62+
});
63+
64+
context('fromJSON', () => {
65+
it('builds the new object correctly', () => {
66+
const cred = SAMLAuthCredential.fromJSON({
67+
signInMethod: 'saml.provider',
68+
providerId: 'saml.provider',
69+
pendingToken: 'pending-token'
70+
});
71+
72+
expect(cred).to.be.instanceOf(SAMLAuthCredential);
73+
expect(cred!.providerId).to.eq('saml.provider');
74+
expect(cred!.signInMethod).to.eq('saml.provider');
75+
});
76+
});
77+
78+
context('#makeRequest', () => {
79+
it('generates the proper request', async () => {
80+
await SAMLAuthCredential._create(
81+
'saml.provider',
82+
'pending-token'
83+
)._getIdTokenResponse(auth);
84+
85+
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
86+
expect(request.requestUri).to.eq('http://localhost');
87+
expect(request.returnSecureToken).to.be.true;
88+
expect(request.pendingToken).to.eq('pending-token');
89+
expect(request.postBody).to.be.undefined;
90+
});
91+
});
92+
93+
context('internal methods', () => {
94+
let cred: SAMLAuthCredential;
95+
96+
beforeEach(() => {
97+
cred = SAMLAuthCredential._create('saml.provider', 'pending-token');
98+
});
99+
100+
it('_getIdTokenResponse calls through correctly', async () => {
101+
await cred._getIdTokenResponse(auth);
102+
103+
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
104+
expect(request.postBody).to.be.undefined;
105+
expect(request.pendingToken).to.eq('pending-token');
106+
});
107+
108+
it('_linkToIdToken sets the idToken field on the request', async () => {
109+
await cred._linkToIdToken(auth, 'new-id-token');
110+
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
111+
expect(request.postBody).to.be.undefined;
112+
expect(request.pendingToken).to.eq('pending-token');
113+
expect(request.idToken).to.eq('new-id-token');
114+
});
115+
116+
it('_getReauthenticationResolver sets autoCreate to false', async () => {
117+
await cred._getReauthenticationResolver(auth);
118+
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
119+
expect(request.postBody).to.be.undefined;
120+
expect(request.pendingToken).to.eq('pending-token');
121+
expect(request.autoCreate).to.be.false;
122+
});
123+
});
124+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
/**
19+
* Represents the SAML credentials returned by an {@link SAMLAuthProvider}.
20+
*
21+
* @public
22+
*/
23+
24+
import {
25+
signInWithIdp,
26+
SignInWithIdpRequest
27+
} from '../../api/authentication/idp';
28+
import { AuthInternal } from '../../model/auth';
29+
import { IdTokenResponse } from '../../model/id_token';
30+
import { AuthCredential } from './auth_credential';
31+
32+
const IDP_REQUEST_URI = 'http://localhost';
33+
34+
/**
35+
* @public
36+
*/
37+
export class SAMLAuthCredential extends AuthCredential {
38+
/** @internal */
39+
private constructor(
40+
providerId: string,
41+
private readonly pendingToken: string
42+
) {
43+
super(providerId, providerId);
44+
}
45+
46+
/** @internal */
47+
_getIdTokenResponse(auth: AuthInternal): Promise<IdTokenResponse> {
48+
const request = this.buildRequest();
49+
return signInWithIdp(auth, request);
50+
}
51+
52+
/** @internal */
53+
_linkToIdToken(
54+
auth: AuthInternal,
55+
idToken: string
56+
): Promise<IdTokenResponse> {
57+
const request = this.buildRequest();
58+
request.idToken = idToken;
59+
return signInWithIdp(auth, request);
60+
}
61+
62+
/** @internal */
63+
_getReauthenticationResolver(auth: AuthInternal): Promise<IdTokenResponse> {
64+
const request = this.buildRequest();
65+
request.autoCreate = false;
66+
return signInWithIdp(auth, request);
67+
}
68+
69+
/** {@inheritdoc AuthCredential.toJSON} */
70+
toJSON(): object {
71+
return {
72+
signInMethod: this.signInMethod,
73+
providerId: this.providerId,
74+
pendingToken: this.pendingToken
75+
};
76+
}
77+
78+
/**
79+
* Static method to deserialize a JSON representation of an object into an
80+
* {@link AuthCredential}.
81+
*
82+
* @param json - Input can be either Object or the stringified representation of the object.
83+
* When string is provided, JSON.parse would be called first.
84+
*
85+
* @returns If the JSON input does not represent an {@link AuthCredential}, null is returned.
86+
*/
87+
static fromJSON(json: string | object): SAMLAuthCredential | null {
88+
const obj = typeof json === 'string' ? JSON.parse(json) : json;
89+
const {
90+
providerId,
91+
signInMethod,
92+
pendingToken
93+
}: Record<string, string> = obj;
94+
if (
95+
!providerId ||
96+
!signInMethod ||
97+
!pendingToken ||
98+
providerId !== signInMethod
99+
) {
100+
return null;
101+
}
102+
103+
return new SAMLAuthCredential(providerId, pendingToken);
104+
}
105+
106+
/**
107+
* Helper static method to avoid exposing the constructor to end users.
108+
*
109+
* @internal
110+
*/
111+
static _create(providerId: string, pendingToken: string): SAMLAuthCredential {
112+
return new SAMLAuthCredential(providerId, pendingToken);
113+
}
114+
115+
private buildRequest(): SignInWithIdpRequest {
116+
return {
117+
requestUri: IDP_REQUEST_URI,
118+
returnSecureToken: true,
119+
pendingToken: this.pendingToken
120+
};
121+
}
122+
}

packages-exp/auth-exp/src/core/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,11 @@ export { inMemoryPersistence } from './persistence/in_memory';
148148
// providers
149149
export { EmailAuthProvider } from './providers/email';
150150
export { FacebookAuthProvider } from './providers/facebook';
151+
export { CustomParameters } from './providers/federated';
151152
export { GoogleAuthProvider } from './providers/google';
152153
export { GithubAuthProvider } from './providers/github';
153-
export {
154-
OAuthProvider,
155-
CustomParameters,
156-
OAuthCredentialOptions
157-
} from './providers/oauth';
154+
export { OAuthProvider, OAuthCredentialOptions } from './providers/oauth';
155+
export { SAMLAuthProvider } from './providers/saml';
158156
export { TwitterAuthProvider } from './providers/twitter';
159157

160158
// strategies

packages-exp/auth-exp/src/core/providers/facebook.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { FirebaseError } from '@firebase/util';
2525
import { TaggedWithTokenResponse } from '../../model/id_token';
2626
import { UserCredentialInternal } from '../../model/user';
2727
import { OAuthCredential } from '../credentials/oauth';
28-
import { OAuthProvider } from './oauth';
28+
import { BaseOAuthProvider } from './oauth';
2929

3030
/**
3131
* Provider for generating an {@link OAuthCredential} for {@link ProviderId.FACEBOOK}.
@@ -66,7 +66,7 @@ import { OAuthProvider } from './oauth';
6666
*
6767
* @public
6868
*/
69-
export class FacebookAuthProvider extends OAuthProvider {
69+
export class FacebookAuthProvider extends BaseOAuthProvider {
7070
/** Always set to {@link SignInMethod.FACEBOOK}. */
7171
static readonly FACEBOOK_SIGN_IN_METHOD = SignInMethod.FACEBOOK;
7272
/** Always set to {@link ProviderId.FACEBOOK}. */
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 } from 'chai';
19+
import { FederatedAuthProvider } from './federated';
20+
21+
/** Federated provider is marked abstract; create a pass-through class */
22+
class SimpleFederatedProvider extends FederatedAuthProvider {}
23+
24+
describe('core/providers/federated', () => {
25+
let federatedProvider: FederatedAuthProvider;
26+
27+
beforeEach(() => {
28+
federatedProvider = new SimpleFederatedProvider('federated');
29+
});
30+
31+
it('has the providerId', () => {
32+
expect(federatedProvider.providerId).to.eq('federated');
33+
});
34+
35+
it('allows setting a default language code', () => {
36+
expect(federatedProvider.defaultLanguageCode).to.be.null;
37+
federatedProvider.setDefaultLanguage('en-US');
38+
expect(federatedProvider.defaultLanguageCode).to.eq('en-US');
39+
});
40+
41+
it('can set and retrieve custom parameters', () => {
42+
expect(federatedProvider.getCustomParameters()).to.eql({});
43+
expect(federatedProvider.setCustomParameters({ foo: 'bar' })).to.eq(
44+
federatedProvider
45+
);
46+
expect(federatedProvider.getCustomParameters()).to.eql({ foo: 'bar' });
47+
});
48+
});

0 commit comments

Comments
 (0)