Skip to content

Commit

Permalink
[Auth] Add SAML support to the new SDK (#4619)
Browse files Browse the repository at this point in the history
* Initial saml support

* Round out saml support w/ tests; break out oauth providers

* Formatting, license

* Fix tests

* Formatting
  • Loading branch information
sam-gc authored Mar 12, 2021
1 parent 8dab8e1 commit 1de07ba
Show file tree
Hide file tree
Showing 15 changed files with 599 additions and 102 deletions.
124 changes: 124 additions & 0 deletions packages-exp/auth-exp/src/core/credentials/saml.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect } from 'chai';

import { mockEndpoint } from '../../../test/helpers/api/helper';
import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as fetch from '../../../test/helpers/mock_fetch';
import { Endpoint } from '../../api';
import { SignInWithIdpRequest } from '../../api/authentication/idp';
import { SAMLAuthCredential } from './saml';

describe('core/credentials/saml', () => {
let auth: TestAuth;
let signInWithIdp: fetch.Route;

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

signInWithIdp = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, {
...TEST_ID_TOKEN_RESPONSE
});
});

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

context('_create', () => {
it('sets the provider', () => {
const cred = SAMLAuthCredential._create('saml.provider', 'pending-token');
expect(cred.providerId).to.eq('saml.provider');
});
});

context('#toJSON', () => {
it('packs up everything', () => {
const cred = SAMLAuthCredential._create('saml.provider', 'pending-token');

expect(cred.toJSON()).to.eql({
signInMethod: 'saml.provider',
providerId: 'saml.provider',
pendingToken: 'pending-token'
});
});
});

context('fromJSON', () => {
it('builds the new object correctly', () => {
const cred = SAMLAuthCredential.fromJSON({
signInMethod: 'saml.provider',
providerId: 'saml.provider',
pendingToken: 'pending-token'
});

expect(cred).to.be.instanceOf(SAMLAuthCredential);
expect(cred!.providerId).to.eq('saml.provider');
expect(cred!.signInMethod).to.eq('saml.provider');
});
});

context('#makeRequest', () => {
it('generates the proper request', async () => {
await SAMLAuthCredential._create(
'saml.provider',
'pending-token'
)._getIdTokenResponse(auth);

const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
expect(request.requestUri).to.eq('http://localhost');
expect(request.returnSecureToken).to.be.true;
expect(request.pendingToken).to.eq('pending-token');
expect(request.postBody).to.be.undefined;
});
});

context('internal methods', () => {
let cred: SAMLAuthCredential;

beforeEach(() => {
cred = SAMLAuthCredential._create('saml.provider', 'pending-token');
});

it('_getIdTokenResponse calls through correctly', async () => {
await cred._getIdTokenResponse(auth);

const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
expect(request.postBody).to.be.undefined;
expect(request.pendingToken).to.eq('pending-token');
});

it('_linkToIdToken sets the idToken field on the request', async () => {
await cred._linkToIdToken(auth, 'new-id-token');
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
expect(request.postBody).to.be.undefined;
expect(request.pendingToken).to.eq('pending-token');
expect(request.idToken).to.eq('new-id-token');
});

it('_getReauthenticationResolver sets autoCreate to false', async () => {
await cred._getReauthenticationResolver(auth);
const request = signInWithIdp.calls[0].request as SignInWithIdpRequest;
expect(request.postBody).to.be.undefined;
expect(request.pendingToken).to.eq('pending-token');
expect(request.autoCreate).to.be.false;
});
});
});
122 changes: 122 additions & 0 deletions packages-exp/auth-exp/src/core/credentials/saml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Represents the SAML credentials returned by an {@link SAMLAuthProvider}.
*
* @public
*/

import {
signInWithIdp,
SignInWithIdpRequest
} from '../../api/authentication/idp';
import { AuthInternal } from '../../model/auth';
import { IdTokenResponse } from '../../model/id_token';
import { AuthCredential } from './auth_credential';

const IDP_REQUEST_URI = 'http://localhost';

/**
* @public
*/
export class SAMLAuthCredential extends AuthCredential {
/** @internal */
private constructor(
providerId: string,
private readonly pendingToken: string
) {
super(providerId, providerId);
}

/** @internal */
_getIdTokenResponse(auth: AuthInternal): Promise<IdTokenResponse> {
const request = this.buildRequest();
return signInWithIdp(auth, request);
}

/** @internal */
_linkToIdToken(
auth: AuthInternal,
idToken: string
): Promise<IdTokenResponse> {
const request = this.buildRequest();
request.idToken = idToken;
return signInWithIdp(auth, request);
}

/** @internal */
_getReauthenticationResolver(auth: AuthInternal): Promise<IdTokenResponse> {
const request = this.buildRequest();
request.autoCreate = false;
return signInWithIdp(auth, request);
}

/** {@inheritdoc AuthCredential.toJSON} */
toJSON(): object {
return {
signInMethod: this.signInMethod,
providerId: this.providerId,
pendingToken: this.pendingToken
};
}

/**
* Static method to deserialize a JSON representation of an object into an
* {@link AuthCredential}.
*
* @param json - Input can be either Object or the stringified representation of the object.
* When string is provided, JSON.parse would be called first.
*
* @returns If the JSON input does not represent an {@link AuthCredential}, null is returned.
*/
static fromJSON(json: string | object): SAMLAuthCredential | null {
const obj = typeof json === 'string' ? JSON.parse(json) : json;
const {
providerId,
signInMethod,
pendingToken
}: Record<string, string> = obj;
if (
!providerId ||
!signInMethod ||
!pendingToken ||
providerId !== signInMethod
) {
return null;
}

return new SAMLAuthCredential(providerId, pendingToken);
}

/**
* Helper static method to avoid exposing the constructor to end users.
*
* @internal
*/
static _create(providerId: string, pendingToken: string): SAMLAuthCredential {
return new SAMLAuthCredential(providerId, pendingToken);
}

private buildRequest(): SignInWithIdpRequest {
return {
requestUri: IDP_REQUEST_URI,
returnSecureToken: true,
pendingToken: this.pendingToken
};
}
}
8 changes: 3 additions & 5 deletions packages-exp/auth-exp/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,11 @@ export { inMemoryPersistence } from './persistence/in_memory';
// providers
export { EmailAuthProvider } from './providers/email';
export { FacebookAuthProvider } from './providers/facebook';
export { CustomParameters } from './providers/federated';
export { GoogleAuthProvider } from './providers/google';
export { GithubAuthProvider } from './providers/github';
export {
OAuthProvider,
CustomParameters,
OAuthCredentialOptions
} from './providers/oauth';
export { OAuthProvider, OAuthCredentialOptions } from './providers/oauth';
export { SAMLAuthProvider } from './providers/saml';
export { TwitterAuthProvider } from './providers/twitter';

// strategies
Expand Down
4 changes: 2 additions & 2 deletions packages-exp/auth-exp/src/core/providers/facebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { FirebaseError } from '@firebase/util';
import { TaggedWithTokenResponse } from '../../model/id_token';
import { UserCredentialInternal } from '../../model/user';
import { OAuthCredential } from '../credentials/oauth';
import { OAuthProvider } from './oauth';
import { BaseOAuthProvider } from './oauth';

/**
* Provider for generating an {@link OAuthCredential} for {@link ProviderId.FACEBOOK}.
Expand Down Expand Up @@ -66,7 +66,7 @@ import { OAuthProvider } from './oauth';
*
* @public
*/
export class FacebookAuthProvider extends OAuthProvider {
export class FacebookAuthProvider extends BaseOAuthProvider {
/** Always set to {@link SignInMethod.FACEBOOK}. */
static readonly FACEBOOK_SIGN_IN_METHOD = SignInMethod.FACEBOOK;
/** Always set to {@link ProviderId.FACEBOOK}. */
Expand Down
48 changes: 48 additions & 0 deletions packages-exp/auth-exp/src/core/providers/federated.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect } from 'chai';
import { FederatedAuthProvider } from './federated';

/** Federated provider is marked abstract; create a pass-through class */
class SimpleFederatedProvider extends FederatedAuthProvider {}

describe('core/providers/federated', () => {
let federatedProvider: FederatedAuthProvider;

beforeEach(() => {
federatedProvider = new SimpleFederatedProvider('federated');
});

it('has the providerId', () => {
expect(federatedProvider.providerId).to.eq('federated');
});

it('allows setting a default language code', () => {
expect(federatedProvider.defaultLanguageCode).to.be.null;
federatedProvider.setDefaultLanguage('en-US');
expect(federatedProvider.defaultLanguageCode).to.eq('en-US');
});

it('can set and retrieve custom parameters', () => {
expect(federatedProvider.getCustomParameters()).to.eql({});
expect(federatedProvider.setCustomParameters({ foo: 'bar' })).to.eq(
federatedProvider
);
expect(federatedProvider.getCustomParameters()).to.eql({ foo: 'bar' });
});
});
Loading

0 comments on commit 1de07ba

Please sign in to comment.