Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement full signOut flow #8

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ You will need the [config](https://github.com/IdentityModel/oidc-client-js/wiki#
Example using `react-router`

```jsx
import { makeAuthenticator, makeUserManager, Callback } from 'react-oidc'
import {
makeAuthenticator,
makeUserManager,
Callback,
SignoutCallback
} from 'react-oidc'

// supply this yourself
import userManagerConfig from '../config'
Expand All @@ -39,6 +44,15 @@ export default () => (
/>
)}
/>
<Route
path="/logout"
render={routeProps => (
<SignoutCallback
onSuccess={() => routeProps.history.push('/')}
userManager={userManager}
/>
)}
/>
<AppWithAuth />
</Switch>
</Router>
Expand All @@ -47,11 +61,12 @@ export default () => (

## Slow start

There are 3 main parts to this library:
There are 4 main parts to this library:

- `makeUserManager` function;
- `makeAuthenticator` function;
- `Callback` component
- `Callback` component;
- `SignoutCallback` component

### `makeUserManager(config)`

Expand Down Expand Up @@ -95,12 +110,25 @@ The lifecycle of this component is as follows:

The `Callback` component will call the `.signinRedirectCallback()` method from `UserManager` and if successful, call the `onSuccess` prop. On error it will call the `onError` prop. You should pass the same instance of `UserManager` that you passed to `makeAuthenticator`.

### `<SignoutCallback />`

| Prop | Type | Required | Default Value | Description |
| ------------- | ------------- | -------- | ------------- | ------------------------------------------------------------------------------------------------ |
| `userManager` | `UserManager` | Yes | `undefined` | `UserManager` instance (the result of `makeUserManager()`) |
| `children` | Component | No | null | Optional component to render at the redirect page |
| `onError` | function | No | `undefined` | Optional callback if there is an error from the Promise returned by `.signoutRedirectCallback()` |
| `onSuccess` | function | No | `undefined` | Optional callback when the Promise from `.signoutRedirectCallback()` resolves |

The `SignoutCallback` component will call the `.signoutRedirectCallback()` method from `UserManager` and if successful, call the `onSuccess` prop. On error it will call the `onError` prop. You should pass the same instance of `UserManager` that you passed to `makeAuthenticator`.

### `<UserData />`

This component exposes the data of the authenticated user. If you are familiar with React's Context API (the official v16.3.x one), this component is just a `Context`.

```jsx
<UserData.Consumer>{context => <p>{context.user.id_token}</p>}</UserData.Consumer>
<UserData.Consumer>
{context => <p>{context.user.id_token}</p>}
</UserData.Consumer>
```

Render prop function
Expand All @@ -117,4 +145,4 @@ This library is deliberately unopinionated about routing, however there are rest

1. **There will be url redirects**. It is highly recommended to use a routing library like `react-router` to help deal with this.

2. **The `redirect_uri` should match eagerly**. You should not render the result of `makeAuthenticator()()` at the location of the `redirect_uri`. If you do, you will end up in a redirect loop that ultimately leads you back to the authentication page. In the quick start above, a `Switch` from `react-router` is used, and the `Callback` component is placed _before_ `AppWithAuth`. This ensures that when the user is redirected to the `redirect_uri`, `AppWithAuth` is not rendered. Once the user data has been loaded into storage, `onSuccess` is called and the user is redirected back to a protected route. When `AppWithAuth` loads now, the valid user session is in storage and the protected routes are rendered.
2. **The `redirect_uri` should match eagerly (for both login and logout)**. You should not render the result of `makeAuthenticator()()` at the location of the `redirect_uri`. If you do, you will end up in a redirect loop that ultimately leads you back to the authentication page. In the quick start above, a `Switch` from `react-router` is used, and the `Callback` component is placed _before_ `AppWithAuth`. This ensures that when the user is redirected to the `redirect_uri`, `AppWithAuth` is not rendered. Once the user data has been loaded into storage, `onSuccess` is called and the user is redirected back to a protected route. When `AppWithAuth` loads now, the valid user session is in storage and the protected routes are rendered.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-oidc",
"version": "0.3.0",
"version": "0.4.0-alpha.0",
"description": "Wrapper for the OIDC JavaScript client, to be used in React projects.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
61 changes: 24 additions & 37 deletions src/Callback/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,58 @@
import * as React from 'react'
import { render } from 'react-testing-library'
import { render, wait } from 'react-testing-library'

import Callback from './'
import MockUserManager from '../utils/userManager'
import makeUserManager from '../makeUserManager'

const USER_MANAGER_CONFIG = {} as any
const makeMockedPromise = (params?: {
result?: 'resolve' | 'reject'
payload?: any
}) => {
if (!params)
params = {
result: 'resolve',
payload: undefined
}
if (params.result === 'resolve') {
return jest.fn(() => Promise.resolve(params.payload))
} else {
return jest.fn(() => Promise.reject(params.payload))
}
}

describe('Callback component', () => {
it('calls onSuccess', async () => {
const signinPromise = () => new Promise(res => res('mockUser'))
it('calls onSuccess', () => {
const onSuccess = jest.fn()

render(
<Callback
userManager={makeUserManager(
{
...USER_MANAGER_CONFIG,
signinRedirectCallback: signinPromise
},
MockUserManager
)}
redirectCallback={makeMockedPromise({ payload: 'mockUser' })}
onSuccess={onSuccess}
>
<div />
</Callback>
)

await signinPromise
expect(onSuccess).toHaveBeenCalledWith('mockUser')
wait(() => expect(onSuccess).toHaveBeenCalledWith('mockUser'))
})

it('calls on Error', async () => {
const signinPromise = () => new Promise((res, rej) => rej('Test Error'))
it('calls on Error', () => {
const onError = jest.fn()

render(
<Callback
userManager={makeUserManager(
{
...USER_MANAGER_CONFIG,
signinRedirectCallback: signinPromise
},
MockUserManager
)}
redirectCallback={makeMockedPromise({ payload: 'Test Error' })}
onError={onError}
>
<div />
</Callback>
)

try {
await signinPromise
} catch (e) {
expect(onError).toHaveBeenCalledWith('Test Error')
}
wait(() => expect(onError).toHaveBeenCalledWith('Test Error'))
})

it('renders children', () => {
const { getByText } = render(
<Callback
userManager={makeUserManager(USER_MANAGER_CONFIG, MockUserManager)}
>
<Callback redirectCallback={makeMockedPromise()}>
<div>Test Child</div>
</Callback>
)
Expand All @@ -70,10 +61,6 @@ describe('Callback component', () => {
})

it('works without children', () => {
render(
<Callback
userManager={makeUserManager(USER_MANAGER_CONFIG, MockUserManager)}
/>
)
render(<Callback redirectCallback={makeMockedPromise()} />)
})
})
18 changes: 13 additions & 5 deletions src/Callback/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import * as React from 'react'
import { User, UserManager, UserManagerSettings } from 'oidc-client'
import { User, UserManager } from 'oidc-client'

export interface ICallbackProps {
export interface ICallbackHandlers {
onSuccess?: (user: User) => void
onError?: (err: any) => void
}
export interface IRedirectCallback {
redirectCallback:
| UserManager['signinRedirectCallback']
| UserManager['signoutRedirectCallback']
}
export type ICallbackProps = ICallbackHandlers & IRedirectCallback
export type ICallbackActionProps = ICallbackHandlers & {
userManager: UserManager
}

class Callback extends React.Component<ICallbackProps> {
public componentDidMount() {
const { onSuccess, onError, userManager } = this.props
const { onSuccess, onError, redirectCallback } = this.props

const um = userManager
um.signinRedirectCallback()
redirectCallback()
.then(user => {
if (onSuccess) {
onSuccess(user)
Expand Down
32 changes: 31 additions & 1 deletion src/RedirectToAuth/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,37 @@ describe('RedirectToAuth', () => {
it('calls signinRedirect', () => {
const mock = jest.fn()
const um = new MockUserManager({ signinRedirectFunction: mock })
render(<RedirectToAuth userManager={um} />)
render(<RedirectToAuth userManager={um} onSilentSuccess={jest.fn()} />)

wait(() => expect(mock).toHaveBeenCalledTimes(1))
})

it('calls onSilentSuccess', () => {
const mock = jest.fn()
const onSilentSuccess = jest.fn()
const um = new MockUserManager({
signinRedirectFunction: mock,
signinSilent: mock
})
render(
<RedirectToAuth userManager={um} onSilentSuccess={onSilentSuccess} />
)

wait(() => expect(onSilentSuccess).toHaveBeenCalledTimes(1))
})

it('calls signinRedirect if signinSilent fails', () => {
const mock = jest.fn()
const onSilentSuccess = jest.fn(() => {
throw new Error()
})
const um = new MockUserManager({
signinRedirectFunction: mock,
signinSilent: jest.fn()
})
render(
<RedirectToAuth userManager={um} onSilentSuccess={onSilentSuccess} />
)

wait(() => expect(mock).toHaveBeenCalledTimes(1))
})
Expand Down
15 changes: 15 additions & 0 deletions src/SignInCallback/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'

import Callback, { ICallbackActionProps } from '../Callback'

const SignInCallback = (props: ICallbackActionProps) => {
return (
<Callback
onSuccess={props.onSuccess}
onError={props.onError}
redirectCallback={props.userManager.signinRedirectCallback}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to props.userManager.signinRedirectCallback.bind(props.userManager)

/>
)
}

export default SignInCallback
15 changes: 15 additions & 0 deletions src/SignOutCallback/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react'

import Callback, { ICallbackActionProps } from '../Callback'

const SignOutCallback = (props: ICallbackActionProps) => {
return (
<Callback
onSuccess={props.onSuccess}
onError={props.onError}
redirectCallback={props.userManager.signoutRedirectCallback}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

/>
)
}

export default SignOutCallback
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { AuthenticatorContext as UserData, makeAuthenticator } from './makeAuth'
import makeUserManager from './makeUserManager'
import Callback from './Callback'
import Callback from './SignInCallback'
import SignoutCallback from './SignOutCallback'

export { Callback, UserData, makeAuthenticator, makeUserManager }
export {
Callback,
SignoutCallback,
UserData,
makeAuthenticator,
makeUserManager
}
10 changes: 8 additions & 2 deletions src/makeAuth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ function makeAuthenticator({

public signOut = () => {
this.userManager.removeUser()
this.getUser()
this.setState(({ context }) => ({
context: { ...context, user: null },
isFetchingUser: false
}))
this.userManager.signoutRedirect()
}

public isValid = () => {
Expand All @@ -93,7 +97,9 @@ function makeAuthenticator({
return placeholderComponent || null
}
return this.isValid() ? (
<AuthenticatorContext.Provider value={this.state.context}>{WrappedComponent}</AuthenticatorContext.Provider>
<AuthenticatorContext.Provider value={this.state.context}>
{WrappedComponent}
</AuthenticatorContext.Provider>
) : (
<RedirectToAuth
userManager={this.userManager}
Expand Down
18 changes: 16 additions & 2 deletions src/utils/userManager.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { UserManagerSettings } from 'oidc-client'

export interface IMockUserManagerOptions extends UserManagerSettings {
export interface IOverloads {
getUserFunction: () => Promise<any>
signinRedirectCallback: () => Promise<any>
signinRedirectFunction: () => void
signoutRedirectFunction: () => void
signinSilent?: () => void
}
class UserManager {
export interface IMockUserManagerOptions
extends UserManagerSettings,
IOverloads {}
class UserManager implements IOverloads {
getUserFunction: () => Promise<any>
signinRedirectCallbackFunction: () => Promise<any>
signinRedirectFunction: () => void
signoutRedirectFunction: () => void
signinSilent?: () => void

constructor(args: IMockUserManagerOptions) {
this.getUserFunction = args.getUserFunction
this.signinRedirectFunction = args.signinRedirectFunction
this.signinRedirectCallbackFunction = args.signinRedirectCallback
this.signoutRedirectFunction = args.signoutRedirectFunction
this.signinSilent = args.signinSilent
}

getUser() {
Expand All @@ -32,6 +41,11 @@ class UserManager {
? this.signinRedirectCallbackFunction()
: new Promise(res => res())
}
signoutRedirect(): void {
return this.signoutRedirectFunction
? this.signoutRedirectFunction()
: undefined
}
}

export default UserManager as any