Skip to content

Commit 43cc95f

Browse files
committed
Refactored auth into separate provider
Using the higher-order component model described in React docs to "wrap" those components that need auth.
1 parent 38c6151 commit 43cc95f

File tree

6 files changed

+411
-262
lines changed

6 files changed

+411
-262
lines changed

demo/graph-tutorial/src/App.tsx

Lines changed: 17 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,184 +1,53 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
import React, { Component } from 'react';
4-
import { BrowserRouter as Router, Route } from 'react-router-dom';
4+
import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom';
55
import { Container } from 'reactstrap';
6-
import { UserAgentApplication } from 'msal';
6+
import withAuthProvider, { AuthComponentProps } from './AuthProvider';
77
import NavBar from './NavBar';
88
import ErrorMessage from './ErrorMessage';
99
import Welcome from './Welcome';
1010
import Calendar from './Calendar';
11-
import { config } from './Config';
12-
import { getUserDetails } from './GraphService';
1311
import 'bootstrap/dist/css/bootstrap.css';
1412

15-
interface AppState {
16-
error: any;
17-
isAuthenticated: boolean;
18-
user: any;
19-
}
20-
21-
class App extends Component<any, AppState> {
22-
private userAgentApplication: UserAgentApplication;
23-
24-
// <constructorSnippet>
25-
constructor(props: any) {
26-
super(props);
27-
28-
this.userAgentApplication = new UserAgentApplication({
29-
auth: {
30-
clientId: config.appId,
31-
redirectUri: config.redirectUri
32-
},
33-
cache: {
34-
cacheLocation: "sessionStorage",
35-
storeAuthStateInCookie: true
36-
}
37-
});
38-
39-
var account = this.userAgentApplication.getAccount();
40-
41-
this.state = {
42-
isAuthenticated: (account !== null),
43-
user: {},
44-
error: null
45-
};
46-
47-
if (account) {
48-
// Enhance user object with data from Graph
49-
this.getUserProfile();
50-
}
51-
}
52-
// </constructorSnippet>
53-
13+
class App extends Component<AuthComponentProps> {
5414
render() {
5515
let error = null;
56-
if (this.state.error) {
57-
error = <ErrorMessage message={this.state.error.message} debug={this.state.error.debug} />;
16+
if (this.props.error) {
17+
error = <ErrorMessage
18+
message={this.props.error.message}
19+
debug={this.props.error.debug} />;
5820
}
5921

6022
// <renderSnippet>
6123
return (
6224
<Router>
6325
<div>
6426
<NavBar
65-
isAuthenticated={this.state.isAuthenticated}
66-
authButtonMethod={this.state.isAuthenticated ? this.logout.bind(this) : this.login.bind(this)}
67-
user={this.state.user}/>
27+
isAuthenticated={this.props.isAuthenticated}
28+
authButtonMethod={this.props.isAuthenticated ? this.props.logout : this.props.login}
29+
user={this.props.user}/>
6830
<Container>
6931
{error}
7032
<Route exact path="/"
7133
render={(props) =>
7234
<Welcome {...props}
73-
isAuthenticated={this.state.isAuthenticated}
74-
user={this.state.user}
75-
authButtonMethod={this.login.bind(this)} />
35+
isAuthenticated={this.props.isAuthenticated}
36+
user={this.props.user}
37+
authButtonMethod={this.props.login} />
7638
} />
7739
<Route exact path="/calendar"
7840
render={(props) =>
79-
<Calendar {...props}
80-
showError={this.setErrorMessage.bind(this)} />
41+
this.props.isAuthenticated ?
42+
<Calendar {...props} /> :
43+
<Redirect to="/" />
8144
} />
8245
</Container>
8346
</div>
8447
</Router>
8548
);
8649
// </renderSnippet>
8750
}
88-
89-
setErrorMessage(message: string, debug: string) {
90-
this.setState({
91-
error: {message: message, debug: debug}
92-
});
93-
}
94-
95-
// <loginSnippet>
96-
async login() {
97-
try {
98-
await this.userAgentApplication.loginPopup(
99-
{
100-
scopes: config.scopes,
101-
prompt: "select_account"
102-
});
103-
await this.getUserProfile();
104-
}
105-
catch(err) {
106-
var error = {};
107-
108-
if (typeof(err) === 'string') {
109-
var errParts = err.split('|');
110-
error = errParts.length > 1 ?
111-
{ message: errParts[1], debug: errParts[0] } :
112-
{ message: err };
113-
} else {
114-
error = {
115-
message: err.message,
116-
debug: JSON.stringify(err)
117-
};
118-
}
119-
120-
this.setState({
121-
isAuthenticated: false,
122-
user: {},
123-
error: error
124-
});
125-
}
126-
}
127-
// </loginSnippet>
128-
129-
// <logoutSnippet>
130-
logout() {
131-
this.userAgentApplication.logout();
132-
}
133-
// </logoutSnippet>
134-
135-
// <getUserProfileSnippet>
136-
async getUserProfile() {
137-
try {
138-
// Get the access token silently
139-
// If the cache contains a non-expired token, this function
140-
// will just return the cached token. Otherwise, it will
141-
// make a request to the Azure OAuth endpoint to get a token
142-
143-
var accessToken = await this.userAgentApplication.acquireTokenSilent({
144-
scopes: config.scopes
145-
});
146-
147-
if (accessToken) {
148-
// Get the user's profile from Graph
149-
var user = await getUserDetails(accessToken.accessToken);
150-
this.setState({
151-
isAuthenticated: true,
152-
user: {
153-
displayName: user.displayName,
154-
email: user.mail || user.userPrincipalName
155-
},
156-
error: null
157-
});
158-
}
159-
}
160-
catch(err) {
161-
var error = {};
162-
if (typeof(err) === 'string') {
163-
var errParts = err.split('|');
164-
error = errParts.length > 1 ?
165-
{ message: errParts[1], debug: errParts[0] } :
166-
{ message: err };
167-
} else {
168-
error = {
169-
message: err.message,
170-
debug: JSON.stringify(err)
171-
};
172-
}
173-
174-
this.setState({
175-
isAuthenticated: false,
176-
user: {},
177-
error: error
178-
});
179-
}
180-
}
181-
// </getUserProfileSnippet>
18251
}
18352

184-
export default App;
53+
export default withAuthProvider(App);
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
import React from 'react';
4+
import { UserAgentApplication } from 'msal';
5+
6+
import { config } from './Config';
7+
import { getUserDetails } from './GraphService';
8+
9+
export interface AuthComponentProps {
10+
error: any;
11+
isAuthenticated: boolean;
12+
user: any;
13+
login: Function;
14+
logout: Function;
15+
getAccessToken: Function;
16+
setError: Function;
17+
}
18+
19+
interface AuthProviderState {
20+
error: any;
21+
isAuthenticated: boolean;
22+
user: any;
23+
}
24+
25+
export default function withAuthProvider<T extends React.Component<AuthComponentProps>>
26+
(WrappedComponent: new(props: AuthComponentProps, context?: any) => T): React.ComponentClass {
27+
return class extends React.Component<any, AuthProviderState> {
28+
private userAgentApplication: UserAgentApplication;
29+
30+
constructor(props: any) {
31+
super(props);
32+
this.state = {
33+
error: null,
34+
isAuthenticated: false,
35+
user: {}
36+
};
37+
38+
// Initialize the MSAL application object
39+
this.userAgentApplication = new UserAgentApplication({
40+
auth: {
41+
clientId: config.appId,
42+
redirectUri: config.redirectUri
43+
},
44+
cache: {
45+
cacheLocation: "sessionStorage",
46+
storeAuthStateInCookie: true
47+
}
48+
});
49+
}
50+
51+
componentDidMount() {
52+
// If MSAL already has an account, the user
53+
// is already logged in
54+
var account = this.userAgentApplication.getAccount();
55+
56+
if (account) {
57+
// Enhance user object with data from Graph
58+
this.getUserProfile();
59+
}
60+
}
61+
62+
render() {
63+
return <WrappedComponent
64+
error = { this.state.error }
65+
isAuthenticated = { this.state.isAuthenticated }
66+
user = { this.state.user }
67+
login = { () => this.login() }
68+
logout = { () => this.logout() }
69+
getAccessToken = { (scopes: string[]) => this.getAccessToken(scopes)}
70+
setError = { (message: string, debug: string) => this.setErrorMessage(message, debug)}
71+
{...this.props} {...this.state} />;
72+
}
73+
74+
async login() {
75+
try {
76+
// Login via popup
77+
await this.userAgentApplication.loginPopup(
78+
{
79+
scopes: config.scopes,
80+
prompt: "select_account"
81+
});
82+
// After login, get the user's profile
83+
await this.getUserProfile();
84+
}
85+
catch(err) {
86+
this.setState({
87+
isAuthenticated: false,
88+
user: {},
89+
error: this.normalizeError(err)
90+
});
91+
}
92+
}
93+
94+
logout() {
95+
this.userAgentApplication.logout();
96+
}
97+
98+
async getAccessToken(scopes: string[]): Promise<string> {
99+
try {
100+
// Get the access token silently
101+
// If the cache contains a non-expired token, this function
102+
// will just return the cached token. Otherwise, it will
103+
// make a request to the Azure OAuth endpoint to get a token
104+
var silentResult = await this.userAgentApplication.acquireTokenSilent({
105+
scopes: scopes
106+
});
107+
108+
return silentResult.accessToken;
109+
} catch (err) {
110+
// If a silent request fails, it may be because the user needs
111+
// to login or grant consent to one or more of the requested scopes
112+
if (this.isInteractionRequired(err)) {
113+
var interactiveResult = await this.userAgentApplication.acquireTokenPopup({
114+
scopes: scopes
115+
});
116+
117+
return interactiveResult.accessToken;
118+
} else {
119+
throw err;
120+
}
121+
}
122+
}
123+
124+
// <getUserProfileSnippet>
125+
async getUserProfile() {
126+
try {
127+
var accessToken = await this.getAccessToken(config.scopes);
128+
129+
if (accessToken) {
130+
// Get the user's profile from Graph
131+
var user = await getUserDetails(accessToken);
132+
this.setState({
133+
isAuthenticated: true,
134+
user: {
135+
displayName: user.displayName,
136+
email: user.mail || user.userPrincipalName
137+
},
138+
error: null
139+
});
140+
}
141+
}
142+
catch(err) {
143+
this.setState({
144+
isAuthenticated: false,
145+
user: {},
146+
error: this.normalizeError(err)
147+
});
148+
}
149+
}
150+
// </getUserProfileSnippet>
151+
152+
setErrorMessage(message: string, debug: string) {
153+
this.setState({
154+
error: {message: message, debug: debug}
155+
});
156+
}
157+
158+
normalizeError(error: string | Error): any {
159+
var normalizedError = {};
160+
if (typeof(error) === 'string') {
161+
var errParts = error.split('|');
162+
normalizedError = errParts.length > 1 ?
163+
{ message: errParts[1], debug: errParts[0] } :
164+
{ message: error };
165+
} else {
166+
normalizedError = {
167+
message: error.message,
168+
debug: JSON.stringify(error)
169+
};
170+
}
171+
return normalizedError;
172+
}
173+
174+
isInteractionRequired(error: Error): boolean {
175+
if (!error.message || error.message.length <= 0) {
176+
return false;
177+
}
178+
179+
return (
180+
error.message.indexOf('consent_required') > -1 ||
181+
error.message.indexOf('interaction_required') > -1 ||
182+
error.message.indexOf('login_required') > -1
183+
);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)