/*
* Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file 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 * as React from 'react';
import {
Amplify,
I18n,
ConsoleLogger as Logger,
Hub,
isEmpty,
} from '@aws-amplify/core';
import { Auth } from '@aws-amplify/auth';
import { Greetings } from './Greetings';
import { SignIn } from './SignIn';
import { ConfirmSignIn } from './ConfirmSignIn';
import { RequireNewPassword } from './RequireNewPassword';
import { SignUp } from './SignUp';
import { Loading } from './Loading';
import { ConfirmSignUp } from './ConfirmSignUp';
import { VerifyContact } from './VerifyContact';
import { ForgotPassword } from './ForgotPassword';
import { TOTPSetup } from './TOTPSetup';
import { Constants } from './common/constants';
import AmplifyTheme from '../Amplify-UI/Amplify-UI-Theme';
import { AmplifyMessageMap } from '../AmplifyMessageMap';
import { Container, Toast } from '../Amplify-UI/Amplify-UI-Components-React';
import { auth } from '../Amplify-UI/data-test-attributes';
import { UsernameAttributes } from './common/types';
const logger = new Logger('Authenticator');
const AUTHENTICATOR_AUTHSTATE = 'amplify-authenticator-authState';
export const EmptyContainer = ({ children }) => <>{children}>;
export interface IAuthenticatorProps {
amplifyConfig?: any;
authData?: any;
authState?: string;
container?: any;
errorMessage?: (message: string) => string;
federated?: any;
hide?: any[];
hideDefault?: boolean;
onStateChange?: (authState: string, data?) => void;
signUpConfig?: any;
theme?: any;
usernameAttributes?: UsernameAttributes;
}
export interface IAuthenticatorState {
authData?;
authState: string;
error?: string;
showToast?: boolean;
}
export class Authenticator extends React.Component<
IAuthenticatorProps,
IAuthenticatorState
> {
public _initialAuthState: string;
public _isMounted: boolean;
constructor(props) {
super(props);
this.handleStateChange = this.handleStateChange.bind(this);
this.handleAuthEvent = this.handleAuthEvent.bind(this);
this.onHubCapsule = this.onHubCapsule.bind(this);
this._initialAuthState = this.props.authState || 'signIn';
this.state = { authState: 'loading' };
Hub.listen('auth', this.onHubCapsule);
}
componentDidMount() {
const config = this.props.amplifyConfig;
if (config) {
Amplify.configure(config);
}
this._isMounted = true;
// The workaround for Cognito Hosted UI:
// Don't check the user immediately if redirected back from Hosted UI as
// it might take some time for credentials to be available, instead
// wait for the hub event sent from Auth module. This item in the
// localStorage is a mark to indicate whether the app is just redirected
// back from Hosted UI or not and is set in Auth:handleAuthResponse.
const byHostedUI = localStorage.getItem(
Constants.REDIRECTED_FROM_HOSTED_UI
);
localStorage.removeItem(Constants.REDIRECTED_FROM_HOSTED_UI);
if (byHostedUI !== 'true') this.checkUser();
}
componentWillUnmount() {
this._isMounted = false;
}
checkUser() {
if (!Auth || typeof Auth.currentAuthenticatedUser !== 'function') {
throw new Error(
'No Auth module found, please ensure @aws-amplify/auth is imported'
);
}
return Auth.currentAuthenticatedUser()
.then(user => {
if (!this._isMounted) {
return;
}
this.handleStateChange('signedIn', user);
})
.catch(err => {
if (!this._isMounted) {
return;
}
let cachedAuthState = null;
try {
cachedAuthState = localStorage.getItem(AUTHENTICATOR_AUTHSTATE);
} catch (e) {
logger.debug('Failed to get the auth state from local storage', e);
}
const promise =
cachedAuthState === 'signedIn' ? Auth.signOut() : Promise.resolve();
promise
.then(() => this.handleStateChange(this._initialAuthState))
.catch(e => {
logger.debug('Failed to sign out', e);
});
});
}
checkContact(user, changeState) {
if (!Auth || typeof Auth.verifiedContact !== 'function') {
throw new Error(
'No Auth module found, please ensure @aws-amplify/auth is imported'
);
}
Auth.verifiedContact(user).then(data => {
if (!isEmpty(data.verified)) {
changeState('signedIn', user);
} else {
user = Object.assign(user, data);
changeState('verifyContact', user);
}
});
}
onHubCapsule(capsule) {
const { channel, payload, source } = capsule;
if (channel === 'auth') {
switch (payload.event) {
case 'cognitoHostedUI':
case 'signIn':
this.checkContact(payload.data, this.handleStateChange);
break;
case 'cognitoHostedUI_failure':
this.handleStateChange('signIn', null);
break;
case 'parsingUrl_failure':
this.handleStateChange('signIn', null);
break;
case 'signOut':
this.handleStateChange('signIn', null);
break;
case 'customGreetingSignOut':
this.handleStateChange('signIn', null);
break;
default:
break;
}
}
}
handleStateChange(state, data?) {
logger.debug('authenticator state change ' + state, data);
if (state === this.state.authState) {
return;
}
if (state === 'signedOut') {
state = 'signIn';
}
try {
localStorage.setItem(AUTHENTICATOR_AUTHSTATE, state);
} catch (e) {
logger.debug('Failed to set the auth state into local storage', e);
}
if (this._isMounted) {
this.setState({
authState: state,
authData: data,
error: null,
showToast: false,
});
}
if (this.props.onStateChange) {
this.props.onStateChange(state, data);
}
}
handleAuthEvent(state, event, showToast = true) {
if (event.type === 'error') {
const map = this.props.errorMessage || AmplifyMessageMap;
const message = typeof map === 'string' ? map : map(event.data);
this.setState({ error: message, showToast });
}
}
render() {
const { authState, authData } = this.state;
const theme = this.props.theme || AmplifyTheme;
const messageMap = this.props.errorMessage || AmplifyMessageMap;
// If container prop is undefined, default to AWS Amplify UI Container
// otherwise if truthy, use the supplied render prop
// otherwise if falsey, use EmptyContainer
const Wrapper =
this.props.container === undefined
? Container
: this.props.container || EmptyContainer;
let {
hideDefault,
hide = [],
federated,
signUpConfig,
usernameAttributes,
} = this.props;
if (hideDefault) {
hide = hide.concat([
Greetings,
SignIn,
ConfirmSignIn,
RequireNewPassword,
SignUp,
ConfirmSignUp,
VerifyContact,
ForgotPassword,
TOTPSetup,
Loading,
]);
}
let props_children = [];
if (typeof this.props.children === 'object') {
if (Array.isArray(this.props.children)) {
props_children = this.props.children;
} else {
props_children.push(this.props.children);
}
}
const default_children = [
,
,
,
,
,
,
,
,
,
,
];
const props_children_override = React.Children.map(
props_children,
child => child.props.override
);
hide = hide.filter(
component => !props_children.find(child => child.type === component)
);
const render_props_children = React.Children.map(
props_children,
(child, index) => {
return React.cloneElement(child, {
key: 'aws-amplify-authenticator-props-children-' + index,
theme,
messageMap,
authState,
authData,
onStateChange: this.handleStateChange,
onAuthEvent: this.handleAuthEvent,
hide,
override: props_children_override,
usernameAttributes,
});
}
);
const render_default_children = hideDefault
? []
: React.Children.map(default_children, (child, index) => {
return React.cloneElement(child, {
key: 'aws-amplify-authenticator-default-children-' + index,
theme,
messageMap,
authState,
authData,
onStateChange: this.handleStateChange,
onAuthEvent: this.handleAuthEvent,
hide,
override: props_children_override,
usernameAttributes,
});
});
const render_children = render_default_children.concat(
render_props_children
);
const error = this.state.error;
return (
{this.state.showToast && (
this.setState({ showToast: false })}
data-test={auth.signIn.signInError}
>
{I18n.get(error)}
)}
{render_children}
);
}
}
/**
* @deprecated use named import
*/
export default Authenticator;