Skip to content
This repository has been archived by the owner on Jun 2, 2021. It is now read-only.

Commit

Permalink
feat: anonymous access (#82)
Browse files Browse the repository at this point in the history
This PR allows unauthenticated requests to be made though the apiClient. There are breaking behavior changes. There are also breaking API changes as well as new functionality contained in this refactor.

**New functionality:**

- `getAuthenticatedUser`: a function that gets the jwt token and returns user information. It will return null if the user is not authenticated. It will not perform a redirect, unlike `ensureAuthenticatedUser`.
- `isPublic` and `isCsrfExempt` options have been added to request configuration for axios requests (get, post, patch, etc). Setting these to true will prevent frontend-auth from attempting to refresh the jwt access token or a csrf token respectively.

BREAKING CHANGE: (Behavior Change) Frontend-auth intercepts outbound requests and attempts to refresh the jwt token if it does not exist or is expired. In the case of a 401 response indicating that the user is logged out, frontend auth will not redirect the user to login, and will allow the outbound request to proceed. Prior behavior: Upon receiving a 401 response, frontend-auth would block the request and redirect the user to login.

`ensureAuthenticatedUser` continues to redirect if the user is logged out.

**API Changes**

- `getAuthenticatedAPIClient` has been renamed to `getAuthenticatedApiClient`. Note the capitalization changes: API  > Api.
- `redirectToLogout` (formerly `apiClient.logout`)
- `redirectToLogin` (formerly `apiClient.login`)
- `ensureAuthenticatedUser` (formerly `apiClient.ensureAuthenticatedUser`)

See the updated README for more details.
  • Loading branch information
Adam Butterworth authored Nov 5, 2019
1 parent 1667c95 commit de68ed4
Show file tree
Hide file tree
Showing 22 changed files with 1,507 additions and 471 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
"extends": "eslint-config-edx",
"parser": "babel-eslint",
"rules": {
"no-trailing-spaces": [
"error",
{
"ignoreComments": true
}
],
"import/no-extraneous-dependencies": [
"error",
{
Expand Down
72 changes: 44 additions & 28 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ frontend-auth

frontend-auth simplifies the process of making authenticated API requests to backend edX services by providing common authN/authZ client code that enables the login/logout flow and handles ensuring the presence of a valid `JWT cookie <https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/docs/decisions/0009-jwt-in-session-cookie.rst>`__.

For detailed usage information `read the API doc <docs/api.md>`__

Usage
-----

Expand All @@ -14,36 +16,46 @@ To install frontend-auth into your project:

npm i --save @edx/frontend-auth

``frontend-auth`` uses `axios interceptors <https://github.com/axios/axios#interceptors>`__ to ensure that a valid JWT cookie exists in your user’s browser before making any API requests. If a valid JWT cookie does not exist, it will attempt to obtain a new valid JWT cookie using a refresh token if one exists in cookies. If a refresh token does not exist or the refresh token is not valid the user will be logged out and redirected to a page of your choosing. Instead of referencing axios directly, you should obtain an http client by calling the ``getAuthenticatedAPIClient`` function provided by ``frontend-auth``:
``frontend-auth`` uses `axios interceptors <https://github.com/axios/axios#interceptors>`__ to ensure that a valid JWT cookie exists in your user’s browser before making any API requests. If a valid JWT cookie does not exist, it will attempt to refresh the JWT cookie. Instead of referencing axios directly, you should obtain an http client by calling the ``getAuthenticatedApiClient`` function provided by ``frontend-auth``:

::

import { NewRelicLoggingService } from '@edx/frontend-logging';
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';

const apiClient = getAuthenticatedAPIClient({
appBaseUrl: process.env.BASE_URL,
loginUrl: process.env.LOGIN_URL,
logoutUrl: process.env.LOGOUT_URL,
refreshAccessTokenEndpoint: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
accessTokenCookieName: process.env.ACCESS_TOKEN_COOKIE_NAME,
loggingService: NewRelicLoggingService, // could be any concrete logging service
// handleRefreshAccessTokenFailure is an optional callback
// to handle failures to refresh an access token (the user is likely logged out).
// If no callback is supplied frontend-auth will redirect the user to login.
// handleRefreshAccessTokenFailure: error => {},
});

apiClient.ensureAuthenticatedUser(window.location.pathname)
.then(({ authenticatedUser, decodedAccessToken }) => {
// 1. Successfully resolving the promise means that the user is authenticated and the apiClient is ready to be used.
// 2. ``authenticatedUser`` is an object containing user account data that was stored in the access token.
// 3. You probably won't need ``decodedAccessToken``, but it is included for completeness and is the raw version
// of the data used to create ``authenticatedUser``.
})
.catch(e => {
// throw or handle error
});
import { getAuthenticatedApiClient } from '@edx/frontend-auth';

const apiClient = getAuthenticatedApiClient({
appBaseUrl: process.env.BASE_URL,
loginUrl: process.env.LOGIN_URL,
logoutUrl: process.env.LOGOUT_URL,
refreshAccessTokenEndpoint: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
accessTokenCookieName: process.env.ACCESS_TOKEN_COOKIE_NAME,
csrfTokenApiPath: process.env.CSRF_TOKEN_API_PATH,
loggingService: configuredLoggingService, // see @edx/frontend-logging
});

apiClient.get('https://edx.org/api/v1/user).then((response) => {});

When bootstrapping an application it may be useful to get the user's access token data from the jwt cookie. This can be done using `getAuthenticatedUser` or `ensureAuthenticatedUser`.

::

import { getAuthenticatedUser, ensureAuthenticatedUser } from '@edx/frontend-auth';

apiClient.getAuthenticatedUser()
.then((authenticatedUserAccessToken) => {
// If the authenticatedUserAccessToken is null it means the user is not logged in.
})
.catch(e => {
// There was an unexpected problem
});

apiClient.ensureAuthenticatedUser(window.location.pathname)
.then((authenticatedUserAccessToken) => {
// If the authenticatedUserAccessToken is null it means the user is not logged in and
// will be redirected to login.
})
.catch(e => {
// There was an unexpected problem
});

``frontend-auth`` provides a ``PrivateRoute`` component which can be used along with ``react-router`` to require authentication for specific routes in your app. Here is an example of defining a route that requires authentication:

Expand All @@ -55,14 +67,18 @@ To install frontend-auth into your project:
<PrivateRoute
path="/authenticated"
component={AuthenticatedComponent}
authenticatedAPIClient={apiClient}
redirect={process.env.BASE_URL} // This should be the base URL of your app.
/>
</Switch>
</ConnectedRouter>

``frontend-auth`` also provides Redux actions and a reducer for injecting user profile data into your store.

Doc Generation
--------------

The docs at `docs/api.md <docs/api.md>`__ are generated using JSDoc. Run `npm run docs` to regenerate them.

.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-auth.svg?branch=master
:target: https://travis-ci.org/edx/frontend-auth
.. |Coveralls| image:: https://img.shields.io/coveralls/edx/frontend-auth.svg?branch=master
Expand Down
7 changes: 0 additions & 7 deletions __mocks__/@edx/frontend-logging.js

This file was deleted.

114 changes: 114 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<a name="LoginRedirect"></a>

## LoginRedirect : <code>ReactComponent</code>
**Kind**: global class
<a name="redirectToLogin"></a>

## redirectToLogin(redirectUrl)
Redirect the user to login

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| redirectUrl | <code>string</code> | the url to redirect to after login |

<a name="redirectToLogout"></a>

## redirectToLogout(redirectUrl)
Redirect the user to logout

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| redirectUrl | <code>string</code> | the url to redirect to after logout |

<a name="getAuthenticatedApiClient"></a>

## getAuthenticatedApiClient(config) ⇒ [<code>HttpClient</code>](#HttpClient)
Gets the apiClient singleton which is an axios instance.

**Kind**: global function
**Returns**: [<code>HttpClient</code>](#HttpClient) - Singleton. A configured axios http client

| Param | Type | Description |
| --- | --- | --- |
| config | <code>object</code> | |
| [config.appBaseUrl] | <code>string</code> | |
| [config.authBaseUrl] | <code>string</code> | |
| [config.loginUrl] | <code>string</code> | |
| [config.logoutUrl] | <code>string</code> | |
| [config.loggingService] | <code>object</code> | requires logError and logInfo methods |
| [config.refreshAccessTokenEndpoint] | <code>string</code> | |
| [config.accessTokenCookieName] | <code>string</code> | |
| [config.csrfTokenApiPath] | <code>string</code> | |

<a name="getAuthenticatedUser"></a>

## getAuthenticatedUser() ⇒ [<code>Promise.&lt;UserData&gt;</code>](#UserData) \| <code>Promise.&lt;null&gt;</code>
Gets the authenticated user's access token. Resolves to null if the user is unauthenticated.

**Kind**: global function
**Returns**: [<code>Promise.&lt;UserData&gt;</code>](#UserData) \| <code>Promise.&lt;null&gt;</code> - Resolves to the user's access token if they are logged in.
<a name="ensureAuthenticatedUser"></a>

## ensureAuthenticatedUser(route) ⇒ [<code>Promise.&lt;UserData&gt;</code>](#UserData)
Ensures a user is authenticated. It will redirect to login when not authenticated.

**Kind**: global function

| Param | Type | Description |
| --- | --- | --- |
| route | <code>string</code> | to return user after login when not authenticated. |

<a name="PrivateRoute"></a>

## PrivateRoute() : <code>ReactComponent</code>
**Kind**: global function
<a name="HttpClient"></a>

## HttpClient
A configured axios client. See axios docs for more
info https://github.com/axios/axios. All the functions
below accept isPublic and isCsrfExempt in the request
config options. Setting these to true will prevent this
client from attempting to refresh the jwt access token
or a csrf token respectively.

```
// A public endpoint (no jwt token refresh)
apiClient.get('/path/to/endpoint', { isPublic: true });
```

```
// A csrf exempt endpoint
apiClient.post('/path/to/endpoint', { data }, { isCsrfExempt: true });
```

**Kind**: global typedef
**Properties**

| Name | Type | Description |
| --- | --- | --- |
| get | <code>function</code> | |
| head | <code>function</code> | |
| options | <code>function</code> | |
| delete | <code>function</code> | (csrf protected) |
| post | <code>function</code> | (csrf protected) |
| put | <code>function</code> | (csrf protected) |
| patch | <code>function</code> | (csrf protected) |

<a name="UserData"></a>

## UserData
**Kind**: global typedef
**Properties**

| Name | Type |
| --- | --- |
| userId | <code>string</code> |
| username | <code>string</code> |
| roles | <code>array</code> |
| administrator | <code>bool</code> |

Loading

0 comments on commit de68ed4

Please sign in to comment.