Skip to content

Commit 2638f76

Browse files
author
Adam Butterworth
authored
feat: upgrade frontend-auth with anonymous access capability (#46)
* feat: upgrade frontend-auth with anonymous access capability BREAKING CHANGE: Uses the new api offered by frontend auth. App.apiClient no longer has methods login, logout, getDecodedAccessToken or refreshAccessToken. Refer to edx/frontend-auth#82 for more info. * docs: typo * docs: update redirect description * fix: upgrade frontend-auth
1 parent da11104 commit 2638f76

File tree

8 files changed

+78
-175
lines changed

8 files changed

+78
-175
lines changed

docs/API.rst

+5-4
Original file line numberDiff line numberDiff line change
@@ -495,11 +495,12 @@ Event constant: ``APP_AUTHENTICATED``
495495

496496
The ``authentication`` phase creates an authenticated apiClient and
497497
makes it available at ``App.apiClient`` on the ``App`` singleton. It
498-
also runs ``ensureAuthenticatedUser`` from @edx/frontend-auth and will
498+
also runs ``getAuthenticatedUser`` from @edx/frontend-auth and will
499499
redirect to the login experience if the user does not have a valid
500-
authentication cookie. Finally, it will make authenticated user
501-
information available at ``App.authenticatedUser`` and
502-
``App.decodedAccessToken`` for later use by the application.
500+
authentication cookie and the ``requireAuthenticatedUser``
501+
option is set to true. Finally, it will make authenticated user
502+
information available at ``App.authenticatedUser`` for later use by the
503+
application.
503504

504505
Default behavior is to redirect to a login page during this phase if the
505506
user is not authenticated. This effectively means that the library does

example/index.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ App.subscribe(APP_ERROR, (error) => {
2424

2525
App.initialize({
2626
messages: [],
27-
requireAuthenticatedUser: true,
27+
requireAuthenticatedUser: false,
2828
hydrateAuthenticatedUser: true,
2929
});

package-lock.json

+28-28
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"@commitlint/prompt": "8.1.0",
4040
"@commitlint/prompt-cli": "8.1.0",
4141
"@edx/frontend-analytics": "3.0.0",
42-
"@edx/frontend-auth": "7.0.1",
42+
"@edx/frontend-auth": "9.0.1",
4343
"@edx/frontend-build": "1.3.1",
4444
"@edx/frontend-i18n": "3.0.3",
4545
"@edx/frontend-logging": "3.0.1",
@@ -67,7 +67,7 @@
6767
},
6868
"peerDependencies": {
6969
"@edx/frontend-analytics": "^3.0.0",
70-
"@edx/frontend-auth": "^7.0.1",
70+
"@edx/frontend-auth": "^9.0.0",
7171
"@edx/frontend-i18n": "^3.0.3",
7272
"@edx/frontend-logging": "^3.0.1",
7373
"@edx/paragon": "^7.1.3",

src/AuthenticatedRoute.test.jsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import React from 'react';
22
import { mount } from 'enzyme';
33
import { Router, Route } from 'react-router-dom';
44
import { createBrowserHistory } from 'history';
5+
import { redirectToLogin } from '@edx/frontend-auth';
56

67
import AuthenticatedRoute from './AuthenticatedRoute';
78
import App from './App';
89
import AppContext from './AppContext';
910

11+
jest.mock('@edx/frontend-auth', () => ({
12+
redirectToLogin: jest.fn(),
13+
}));
14+
1015
describe('AuthenticatedRoute', () => {
1116
beforeEach(() => {
12-
App.apiClient = {
13-
login: jest.fn(),
14-
};
1517
App.history = createBrowserHistory();
18+
redirectToLogin.mockReset();
1619
});
1720

1821
it('should call login if not authenticated', () => {
@@ -29,7 +32,7 @@ describe('AuthenticatedRoute', () => {
2932
App.history.push('/authenticated');
3033
mount(component);
3134

32-
expect(App.apiClient.login).toHaveBeenCalledWith('http://localhost/authenticated');
35+
expect(redirectToLogin).toHaveBeenCalledWith('http://localhost/authenticated');
3336
});
3437

3538
it('should not call login if not the current route', () => {
@@ -46,7 +49,7 @@ describe('AuthenticatedRoute', () => {
4649
App.history.push('/');
4750
const wrapper = mount(component);
4851

49-
expect(App.apiClient.login).not.toHaveBeenCalled();
52+
expect(redirectToLogin).not.toHaveBeenCalled();
5053
const element = wrapper.find('p');
5154
expect(element.text()).toEqual('Anonymous'); // This is just a sanity check on our setup.
5255
});
@@ -64,7 +67,7 @@ describe('AuthenticatedRoute', () => {
6467
);
6568
App.history.push('/authenticated');
6669
const wrapper = mount(component);
67-
expect(App.apiClient.login).not.toHaveBeenCalled();
70+
expect(redirectToLogin).not.toHaveBeenCalled();
6871
const element = wrapper.find('p');
6972
expect(element.text()).toEqual('Authenticated');
7073
});

src/data/service.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { redirectToLogin } from '@edx/frontend-auth';
12
import App from '../App';
23
import { camelCaseObject } from '../api';
34

@@ -18,5 +19,5 @@ function breakOnRedirectFromLogin() {
1819

1920
export function loginRedirect() {
2021
breakOnRedirectFromLogin();
21-
App.apiClient.login(global.location.href);
22+
redirectToLogin(global.location.href);
2223
}

src/handlers/authentication.js

+3-80
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,24 @@
11
/* eslint-disable no-param-reassign */
2-
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
2+
import { getAuthenticatedApiClient, getAuthenticatedUser } from '@edx/frontend-auth';
33

44
import { loginRedirect, getAuthenticatedUserAccount } from '../data/service';
55

66
export default async function authentication(app) {
7-
app.apiClient = getAuthenticatedAPIClient({
7+
app.apiClient = getAuthenticatedApiClient({
88
appBaseUrl: app.config.BASE_URL,
9-
authBaseUrl: app.config.LMS_BASE_URL,
109
accessTokenCookieName: app.config.ACCESS_TOKEN_COOKIE_NAME,
11-
userInfoCookieName: app.config.USER_INFO_COOKIE_NAME,
1210
csrfTokenApiPath: app.config.CSRF_TOKEN_API_PATH,
1311
loginUrl: app.config.LOGIN_URL,
1412
logoutUrl: app.config.LOGOUT_URL,
1513
refreshAccessTokenEndpoint: app.config.REFRESH_ACCESS_TOKEN_ENDPOINT,
1614
loggingService: app.loggingService,
1715
});
1816

19-
// NOTE: Remove this "attach" line once frontend-auth gets its own getAuthenticatedUser method.
20-
// eslint-disable-next-line no-use-before-define
21-
attachGetAuthenticatedUser(app.apiClient);
22-
2317
// Get a valid access token for authenticated API access.
24-
const { authenticatedUser, decodedAccessToken } =
25-
await app.apiClient.getAuthenticatedUser(global.location.pathname);
18+
const authenticatedUser = await getAuthenticatedUser();
2619

2720
// Once we have refreshed our authentication, extract it for use later.
2821
app.authenticatedUser = authenticatedUser;
29-
app.decodedAccessToken = decodedAccessToken;
3022

3123
if (app.requireAuthenticatedUser && app.authenticatedUser === null) {
3224
loginRedirect();
@@ -38,72 +30,3 @@ export default async function authentication(app) {
3830
});
3931
}
4032
}
41-
42-
// NOTE: Remove everything below here when frontend-auth gets its own getAuthenticatedUser method.
43-
/* istanbul ignore next */
44-
function getAuthenticatedUserFromDecodedAccessToken(decodedAccessToken) {
45-
/* istanbul ignore next */
46-
if (decodedAccessToken === null) {
47-
throw new Error('Decoded access token is required to get authenticated user.');
48-
}
49-
50-
return {
51-
userId: decodedAccessToken.user_id,
52-
username: decodedAccessToken.preferred_username,
53-
roles: decodedAccessToken.roles ? decodedAccessToken.roles : [],
54-
administrator: decodedAccessToken.administrator,
55-
};
56-
}
57-
/* istanbul ignore next */
58-
function formatAuthenticatedResponse(decodedAccessToken) {
59-
return {
60-
authenticatedUser: getAuthenticatedUserFromDecodedAccessToken(decodedAccessToken),
61-
decodedAccessToken,
62-
};
63-
}
64-
/* istanbul ignore next */
65-
function attachGetAuthenticatedUser(httpClient) {
66-
// Bail if there's a real implementation of getAuthenticatedUser
67-
if (httpClient.getAuthenticatedUser !== undefined) {
68-
return;
69-
}
70-
71-
httpClient.getAuthenticatedUser = () =>
72-
new Promise((resolve, reject) => {
73-
// Validate auth-related cookies are in a consistent state.
74-
const accessToken = httpClient.getDecodedAccessToken();
75-
const tokenExpired = httpClient.isAccessTokenExpired(accessToken);
76-
if (!tokenExpired) {
77-
// We already have valid JWT cookies
78-
resolve(formatAuthenticatedResponse(accessToken));
79-
}
80-
// Attempt to refresh the JWT cookies.
81-
httpClient
82-
.refreshAccessToken()
83-
// Successfully refreshed the JWT cookies
84-
.then((response) => {
85-
const refreshedAccessToken = httpClient.getDecodedAccessToken();
86-
87-
if (refreshedAccessToken === null) {
88-
// This should never happen, but it does. See ARCH-948 for past research into why.
89-
const errorMessage = 'Access token is null after supposedly successful refresh.';
90-
httpClient.loggingService.logError(`frontend-auth: ${errorMessage}`, {
91-
previousAccessToken: accessToken,
92-
axiosResponse: response,
93-
});
94-
reject(new Error(errorMessage));
95-
return;
96-
}
97-
98-
resolve(formatAuthenticatedResponse(refreshedAccessToken));
99-
}).catch((e) => {
100-
if (e.response.status === 401) {
101-
return resolve({
102-
authenticatedUser: null,
103-
decodedAccessToken: null,
104-
});
105-
}
106-
return reject(e);
107-
});
108-
});
109-
}

0 commit comments

Comments
 (0)