Skip to content

Commit a015140

Browse files
authored
Fix Redirect LoginType to authenticate (#4)
* Fix redirect - move acquireToken call to the componentDidMount stage of the component lifecycle so that the state change forces a re-render but all the associated objects are in place (loginRedirect gets called before this.clientApplication is even assigned, so those actions need to come later * Add AuthenticationState Enum and inline loginRedirect. Also remove userInfo from the state object as it does not change and we return it via callback * Fix broken tests - call userInfoCallback inside createUserInfo * Put in dummy data in the unit tests * PR Feedback: Check value of authenticationState
1 parent e17fa47 commit a015140

File tree

2 files changed

+79
-52
lines changed

2 files changed

+79
-52
lines changed

src/index.test.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as ReactDOM from 'react-dom';
66

77
require('jest-localstorage-mock'); // tslint:disable-line
88

9-
import { AzureAD, LoginType } from './index';
9+
import { AuthenticationState, AzureAD, IUserInfo, LoginType } from './index';
1010

1111
Enzyme.configure({ adapter: new Adapter() })
1212

@@ -23,6 +23,7 @@ it('renders without crashing', () => {
2323

2424
it('updates the userInfo state', () => {
2525

26+
let userInfo : IUserInfo = null;
2627
const unauthenticatedFunction = (login: any) => {
2728
return <div><h1> unauthenticatedFunction </h1> </div>
2829
}
@@ -32,17 +33,17 @@ it('updates the userInfo state', () => {
3233
}
3334

3435
const userInfoCallback = (token: any) => {
35-
// Empty
36+
userInfo = token;
3637
}
3738

3839
const wrapper = Enzyme.shallow(
3940
<AzureAD
40-
clientID={'ed236c58-c43b-444c-962c-0bc28a81a753'}
41-
scopes={[' https://login.microsoftonline.com/syncteam14.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_All']}
41+
clientID={'<random-guid>'}
42+
scopes={['openid']}
4243
unauthenticatedFunction={unauthenticatedFunction}
4344
authenticatedFunction={authenticatedFunction}
4445
userInfoCallback={userInfoCallback}
45-
authority={'https://login.microsoftonline.com/syncteam14.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_All'}
46+
authority={null}
4647
type={LoginType.Popup}
4748
/>
4849
).instance() as AzureAD;
@@ -57,10 +58,10 @@ it('updates the userInfo state', () => {
5758

5859
wrapper.createUserInfo("accesstoken", "idtoken", testUser);
5960

60-
expect(wrapper.state.authenticated).toBe(true);
61-
expect(wrapper.state.userInfo ? wrapper.state.userInfo.jwtAccessToken : '').toEqual("accesstoken");
62-
expect(wrapper.state.userInfo ? wrapper.state.userInfo.jwtIdToken : '').toEqual("idtoken");
63-
expect(wrapper.state.userInfo ? wrapper.state.userInfo.user : {}).toEqual(testUser);
61+
expect(userInfo).not.toBeNull();
62+
expect(userInfo.jwtAccessToken).toEqual("accesstoken");
63+
expect(userInfo.jwtIdToken).toEqual("idtoken");
64+
expect(userInfo.user).toEqual(testUser);
6465

6566
});
6667

@@ -74,18 +75,19 @@ it('logs out the user', () => {
7475
return <div><h1> authenticatedFunction </h1> </div>
7576
}
7677

78+
let userInfo : IUserInfo = null;
7779
const userInfoCallback = (token: any) => {
78-
// Empty
80+
userInfo = token;
7981
}
8082

8183
const wrapper = Enzyme.shallow(
8284
<AzureAD
83-
clientID={'ed236c58-c43b-444c-962c-0bc28a81a753'}
84-
graphScopes={[' https://login.microsoftonline.com/syncteam14.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_All']}
85+
clientID={'<random-guid>'}
86+
scopes={['openid']}
8587
unauthenticatedFunction={unauthenticatedFunction}
8688
authenticatedFunction={authenticatedFunction}
8789
userInfoCallback={userInfoCallback}
88-
authority={'https://login.microsoftonline.com/syncteam14.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_All'}
90+
authority={null}
8991
type={LoginType.Popup}
9092
/>
9193
).instance() as AzureAD;
@@ -102,6 +104,5 @@ it('logs out the user', () => {
102104

103105
wrapper.resetUserInfo();
104106

105-
expect(wrapper.state.authenticated).toBe(false);
106-
expect(wrapper.state.userInfo).toBeNull();
107+
expect(wrapper.state.authenticationState).toBe(AuthenticationState.Unauthenticated);
107108
});

src/index.tsx

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ enum LoginType {
99
Redirect,
1010
}
1111

12+
enum AuthenticationState {
13+
Unauthenticated,
14+
Authenticating,
15+
Authenticated,
16+
}
17+
1218
type UserInfoCallback = (token: IUserInfo) => void;
1319

1420
type UnauthenticatedFunction = (login: LoginFunction) => JSX.Element;
@@ -31,8 +37,7 @@ interface IProps {
3137
}
3238

3339
interface IState {
34-
authenticated: boolean,
35-
userInfo: IUserInfo | null,
40+
authenticationState: AuthenticationState,
3641
}
3742

3843
interface IUserInfo {
@@ -41,44 +46,71 @@ interface IUserInfo {
4146
user: Msal.User,
4247
}
4348

49+
interface IRedirectLogin {
50+
error: string,
51+
errorDesc: string,
52+
idToken: string,
53+
tokenType: string,
54+
}
55+
4456
class AzureAD extends React.Component<IProps, IState> {
4557

4658
private clientApplication: Msal.UserAgentApplication;
59+
private redirectLoginInfo: IRedirectLogin;
4760

4861
constructor(props: IProps) {
4962
super(props);
63+
64+
let authenticationState = AuthenticationState.Unauthenticated;
5065
this.clientApplication = new Msal.UserAgentApplication(
5166
props.clientID,
52-
props.authority ? props.authority : null,
53-
this.loginRedirect
67+
props.authority ? props.authority : null,
68+
(errorDesc: string, idToken: string, error: string, tokenType: string) => {
69+
this.redirectLoginInfo = {
70+
error,
71+
errorDesc,
72+
idToken,
73+
tokenType
74+
}
75+
authenticationState = AuthenticationState.Authenticating;
76+
}
5477
);
55-
this.state = {
56-
authenticated: false,
57-
userInfo: null,
58-
};
78+
79+
this.state = { authenticationState };
5980
}
6081

6182
public render() {
62-
if (!this.state.authenticated) {
63-
return this.props.unauthenticatedFunction(this.login);
83+
switch (this.state.authenticationState) {
84+
case AuthenticationState.Authenticated:
85+
return this.props.authenticatedFunction(this.logout);
86+
case AuthenticationState.Authenticating:
87+
// TODO: Add loading callback, componentDidMount will acquire tokens and then re-render
88+
return null;
89+
case AuthenticationState.Unauthenticated:
90+
default:
91+
return this.props.unauthenticatedFunction(this.login);
6492
}
65-
return this.props.authenticatedFunction(this.logout);
6693
}
6794

68-
public createUserInfo = (accessToken: string, idToken: string, msalUser: Msal.User): IUserInfo => {
95+
public componentDidMount() {
96+
if (this.redirectLoginInfo) {
97+
if (this.redirectLoginInfo.idToken) {
98+
this.acquireTokens(this.redirectLoginInfo.idToken);
99+
}
100+
else if (this.redirectLoginInfo.errorDesc || this.redirectLoginInfo.error) {
101+
Logger.error(`Error doing login redirect; errorDescription=${this.redirectLoginInfo.errorDesc}, error=${this.redirectLoginInfo.error}`);
102+
}
103+
}
104+
}
105+
106+
public createUserInfo = (accessToken: string, idToken: string, msalUser: Msal.User): void => {
69107
const user: IUserInfo = {
70108
jwtAccessToken: accessToken,
71109
jwtIdToken: idToken,
72110
user: msalUser
73111
};
74-
this.setState({
75-
authenticated: true,
76-
userInfo: user
77-
});
78-
112+
this.props.userInfoCallback(user);
79113
this.dispatchToProvidedReduxStore(user);
80-
81-
return user;
82114
}
83115

84116
public resetUserInfo = () => {
@@ -87,25 +119,18 @@ class AzureAD extends React.Component<IProps, IState> {
87119
}
88120

89121
this.setState({
90-
authenticated: false,
91-
userInfo: null
122+
authenticationState: AuthenticationState.Unauthenticated,
92123
});
93124
}
94125

95-
private loginRedirect = (errorDesc: string, idToken: string, error: string, tokenType: string) => {
96-
if (idToken) {
97-
this.acquireTokens(idToken);
98-
}
99-
else if (errorDesc || error) {
100-
Logger.error(`Error doing login redirect; errorDescription=${errorDesc}, error=${error}`);
101-
}
102-
}
103-
104126
private acquireTokens = (idToken: string) => {
105127
this.clientApplication.acquireTokenSilent(this.props.scopes)
106128
.then((accessToken: string) => {
107-
const user = this.createUserInfo(accessToken, idToken, this.clientApplication.getUser());
108-
this.props.userInfoCallback(user);
129+
this.createUserInfo(accessToken, idToken, this.clientApplication.getUser());
130+
131+
this.setState({
132+
authenticationState: AuthenticationState.Authenticated,
133+
});
109134
}, (tokenSilentError) => {
110135
Logger.error(`token silent error; ${tokenSilentError}`);
111136
this.clientApplication.acquireTokenPopup(this.props.scopes)
@@ -131,11 +156,12 @@ class AzureAD extends React.Component<IProps, IState> {
131156
};
132157

133158
private logout = () => {
134-
if (!this.state.authenticated) {return;}
135-
else {
136-
this.resetUserInfo();
137-
this.clientApplication.logout();
159+
if (this.state.authenticationState !== AuthenticationState.Authenticated) {
160+
return;
138161
}
162+
163+
this.resetUserInfo();
164+
this.clientApplication.logout();
139165
};
140166

141167
private dispatchToProvidedReduxStore(data: IUserInfo) {
@@ -145,5 +171,5 @@ class AzureAD extends React.Component<IProps, IState> {
145171
}
146172
}
147173

148-
export { AzureAD, LoginType, IUserInfo, UnauthenticatedFunction, LoginFunction, AAD_LOGIN_SUCCESS };
174+
export { AzureAD, AuthenticationState, LoginType, IUserInfo, UnauthenticatedFunction, LoginFunction, AAD_LOGIN_SUCCESS };
149175
export default AzureAD;

0 commit comments

Comments
 (0)