2.2.9 • Published 3 years ago

productive-auth v2.2.9

Weekly downloads
12
License
UNLICENSED
Repository
-
Last release
3 years ago

Authenticate

This package is intended for react developers who wants to connect to their FusionAuth service with ease.

This package is composed of four modular parts:

  • Client - in charge of communicating with the authentication server.
  • Storage - in charge of storing and retrieving the authentication token.
  • Provider - connects the UI library to Authenticate's state.
  • Service - holds the state and logic of Authenticate and brings all the pieces together.

Quick-start

Create a client and a service:

import { createFusionAuthClient } from 'productive-auth/clients/FusionAuth';
import { createService } from 'productive-auth';
import { createLocalForage } from 'productive-auth/storage-providers/LocalForage';

const client = createFusionAuthClient({
  endpoint: process.env.REACT_APP_FUSION_AUTH_ENDPOINT,
  applicationId: process.env.REACT_APP_FUSION_AUTH_APP_ID,
  apiKey: process.env.REACT_APP_FUSION_AUTH_API_KEY,
});

export const authService = createService({
  client,
  storage: createLocalForage('my-app-name'),
});

Note: You can specify different storage destination, read more here.

Connect the service to the app through a context provider:

import React from 'react';
import ReactDOM from 'react-dom';
import { AuthProvider } from 'productive-auth/providers/ReactProvider';
import { authService } from './auth';
import { GreetUser } from './GreetUser.js';

const App = () => (
  <AuthProvider service={authService}>
    <GreetUser />
  </AuthProvider>
);

ReactDOM.render(<App />, document.getElementById('root'));

Use the auth context:

import React from 'react';
import { useAuth } from 'productive-auth/providers/ReactProvider';

const GreetUser = () => {
  const { user, isAuthenticated } = useAuth();
  return <h3>Welcome {isAuthenticated ? user.email : 'Anonymous'}</h3>;
};

export default GreetUser;

Or use the service directly:

import { authService } from './auth';
import actions from 'productive-auth/actions';

export let myToken = authService.state.context.token;
if (!myToken) {
  service.onTransition(state => {
    if (state.context.token) {
      myToken = state.context.token;
    }
  });
  authService.send(actions.refreshToken());
}

Note: Read on to learn more about the service API.

Service

The service is the heart of Authenticate. it holds all the state and logic.

What it actually is an xstate interpreted machine, the machine uses the client you provide it and (optionally) a storage provider to execute actions based on it's state and messages it receives.

Two of the main divisions of the service's API are: actions and selectors to which we'll go in great detail on the next section.

But first as mentioned before our service is really just an xstate interpreted machine and so it has the same API:

Actions

productive-auth/actions are action creators which mean they don't do any thing by themselves, they are just json messages that you can send to the authentication service:

Register

register({ email, password }) => { type: 'REGISTER', payload: { email, password } }

Tries to create a new user on the service.

Post-state: { error } if occur

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { register } = actions;
authService.send(register({ email, password }));

Login

login({ email, password }) => { type: 'LOGIN', payload: { email, password } }

Login tries to log the user in and will store a token in case of success, in case the user has two-factor authentication enabled the function will store twoFactorId.

Post-state: { token } or { twoFactorId } respectively or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { login } = actions;
authService.send(login({ email, password }));

Two-Factor Login

completeTwoFactorAuthentication({ code }) => { type: ' COMPLETE_TWO_FACTOR_AUTHENTICATION', payload: { code } }

If the user enabled two-factor authentication, the login call will return twoFactorId and to finish the login the user must provide a code from his device.

Post-state: { token } or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { completeTwoFactorAuthentication } = actions;
authService.send(completeTwoFactorAuthentication({ code }));

Passwordless Login

startPasswordlessLogin({ email }) => { type: 'START_PASSWORDLESS_LOGIN', payload: { email } }

To start a One-Time-Password login process the user must specify his email, then we use startPasswordlessLogin to send an email with the One-Time-Password.

Post-state: { error } if occur

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { startPasswordlessLogin } = actions;
authService.send(startPasswordlessLogin({ email }));

completePasswordlessLogin({ code }) => { type: 'COMPLETE_PASSWORDLESS_LOGIN', payload: { code } }

After the user has received the email with the One-Time-Password he must provide it in order to complete the login process.

Post-state: { token } or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { completePasswordlessLogin } = actions;
authService.send(completePasswordlessLogin({ code }));

Login With 3rd Party

loginWith({ provider, data: { accessToken } }) => { type: 'LOGIN_WITH_3RD_PARTY', payload: { provider, data: { accessToken } } }

Using third party authentication provider is a bit tricky, each provider may have its own demands. we must first register an app with the authentication provider and use its sdk. After the user approved the authentication and we got whatever data the provider requires we pass it with an appropriate provider code (e.g "FACEBOOK") to our service and let it take it from there.

Note: The provider code is to enable the client to take special action per provider if needed.

Note: Currently available are GOOGLE and FACEBOOK but it can be extended by passing mapProviderToId to createClient.

Post-state: { token } or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { loginWith } = actions;
authService.send(
  loginWithFacebook({ provider: 'FACEBOOK', data: { accessToken } })
);

Logout

logout() => { type: 'LOGOUT' }

Logout removes the token from the store and the storage and tells the service to remove all cookies from the client.

Post-state: { error } if occur

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { logout } = actions;
authService.send(logout());

Refresh Token

refreshToken() => { type: 'REFRESH_TOKEN' }

Try to get a new token from the service. This method takes a refreshToken as input but it is optional if the user is already logged in and there credentials httpOnly cookies set on the http request.

Post-state: { token } or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { refreshToken } = actions;
authService.send(refreshToken());

Forgot Password

forgotPassword({ email }) => { type: 'FORGOT_PASSWORD', payload: { email } }

The user can ask to reset his password via email. forgotPassword takes an email address and ask the service to send the user a link to reset his password.

Post-state: { error } if occur

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { forgotPassword } = actions;
authService.send(forgotPassword({ email }));

Enable Two-Factor Authentication

startEnableTwoFactor() => { type: 'FORGOT_PASSWORD', payload: { secret, secretBase32Encoded } }

When the user choose to enable two-factor authentication we generate a secret which he shares with his favorite two-factor authentication provider (e.g google authenticator).

Post-state: { enableTwoFactorSecret: { secret, secretBase32Encoded } } or { error }

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { startEnableTwoFactor } = actions;
authService.send(startEnableTwoFactor());

completeEnableTwoFactor({ code }) => { type: 'COMPLETE_ENABLE_TWO_FACTOR', payload: { code } }

After the user feed his authenticator the secret, he gets a code in return, we send this code to the service to calibrate with the authenticator. The next time the user will try to login, we'll need the current code from the authenticator in order to complete the authentication process.

Post-state: { error } if occur

import { authService } from './auth';
import actions from 'productive-auth/actions';

const { completeEnableTwoFactor } = actions;
authService.send(completeEnableTwoFactor({ code }));

Note: The user must be logged in in order to enable two-factor authentication.

Selectors

selectors expose the state of the machine plus some derived data like user (from the decoded token):

Note: In order to always get the latest values all selectors are functions that visit the state at the moment of invocation.

loading

Holds the current (very readable) loading message or null.

Among possible values: "Registering", "Logging in with email/username and password", "Logging out", "Refreshing token"

const { loading } = authStore.selectors;
console.log(`The current loading status is: ${loading()}`);

error

Holds the latest javascript error that occurred or null.

const { error } = authStore.selectors;
console.log(`Oh no! an error has occurred: ${error()}`);

token

holds the token in case the user is authenticated, null otherwise.

const { token } = authStore.selectors;
console.log(`The current user's token is: ${token()}`);

twoFactorId

In case the user has two-factor authentication enabled it will hold the login attempt id. Note that the store makes internal use of this variable in the Two-Factor Login action, in most cases this variable should not interest us.

const { twoFactorId } = authStore.selectors;
console.log(
  `The user is trying to login and it has two-factor enabled: ${twoFactorId()}`
);

enableTwoFactorSecret

When the user enables two-factor authentication we generate a secret for him (see enable two-factor). This selector will take care of holding the secret for us so we won't have to :).

const { enableTwoFactorSecret } = authStore.selectors;
const { secret, secretBase32Encoded } = enableTwoFactorSecret();
console.log(
  `This is the secret to enable two-factor authentication: ${secretBase32Encoded}`
);

isAuthenticated

Returns true if the user is authenticated, false otherwise. This state is derived directly from the token selector for convenience and will always be synced with it.

const { isAuthenticated, token } = authStore.selectors;
console.log(`Is the current user authenticated? ${isAuthenticated()}`);
const alwaysTrue = isAuthenticated() === !!token();

user

When the user is authenticated this selector will return an object with the user's details.

const { user } = authStore.selectors;
const { sub, email, roles } = user();
console.log(`The current user's email: ${email}`);
console.log(`The current user's roles: ${roles}`);
console.log(`The current user's subscription id: ${sub}`);

Provider

The provider has a very simple job: to mediate between the store's reactive api and the UI framework\tool of choice (React, Vue, Angular...).

At the moment there is only react provider for reference:

import React, { createContext, useEffect, useContext, useState } from 'react';
import { map } from 'ramda';

const AuthContext = createContext();

export const AuthProvider = ({ store, children }) => {
  const { subscribe, actions, selectors, client, storage } = store;

  const [state, updateState] = useState(map(v => v(), selectors));

  useEffect(() => {
    actions.refreshToken().catch(() => {});
    return subscribe(() => updateState(map(v => v(), selectors)));
  }, []);

  return (
    <AuthContext.Provider value={{ ...state, ...actions, client, storage }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

Note how we use the context API and hook for ease of access to the store from any where on our app. Also note that we map on the selectors and extract the value from the functions, this so that react's useState will be able to pick up on changes to the state of the store.

Client

At the moment there's only one client, FusionAuth's.

But still we aim for it to be very easy to extend this library to work with other authentication services and prepared to make changes on stiff corners so it will be flexible enough.

All you need to do in order to connect this library to another authentication service is to implement this (hopefully) reasonable Interface:

Client API

register({ email, password }) => Promise()

Returns nothing or an error.

Takes the user's credentials (at the moment only email and password but prone to changes) and creates a new user on the service.


login({ email, password }) => Promise({ token, twoFactorId })

Returns { token } or { twoFactorId } respectively or an error.

Takes the user's credentials (at the moment only email and password but prone to changes) and returns a token in case of success, or twoFactorId in case the user has two-factor authentication enabled.


completeTwoFactorAuthentication({ code, twoFactorId }) => Promise({ token })

Returns { token } or an error.

Takes the twoFactorId returned from login and the code from the user's device to complete the login process.


startPasswordlessLogin({ email }) => Promise()

Returns nothing or an error.

Takes the user's email and asks the service to send an email with the One-Time-Password.


completePasswordlessLogin({ code }) => Promise({ token })

Returns { token } or an error.

Takes the One-Time-Password from the user and returns a token to complete the authentication process.


loginWithFacebook({ accessToken }) => Promise({ token })

Returns { token } or an error.

Takes an accessToken and authenticate with the service to get a JWT. note that the service should be able to merge users by some identifier (probably email) so that users that logged in with email and then authenticated using facebook won't have to lose all their data.


logout() => Promise()

Returns nothing or an error.

Logout tells the service to remove all cookies from the client. This assumes that the service is using httpOnly cookies to hold credentials and a refreshToken for persisting sessions.


refreshToken() => Promise({ token })

Returns { token } or an error.

refreshToken should return a new token in case the user's credentials are in its cookies error otherwise.


forgotPassword({ email }) => Promise()

Returns nothing or an error.

Takes an email address and ask the service to send the user a link to reset his password.


startEnableTwoFactor() => Promise({ secret, secretBase32Encoded })

Returns { secret, secretBase32Encoded } or an error.

This method should generate a secret and return it and its Base32Encoded version.


enableTwoFactor({ code, secret }) => Promise()

Returns nothing or an error.

Takes the secret generated by startEnableTwoFactor and the code provided by the user to calibrate the service to the user's device and enable two-factor authentication.

Storage

Storage providers are there to enable the developers to control where the token is stored besides on memory. This is useful for those who wants to hold their token on local-storage cause their authentication service does not support httpOnly refreshToken cookie, or for those who want to encrypt their token and keep it somewhere close. Please note that some storage types are considered not safe and prone to attacks, that is why the default storage is on-memory only.

That said the library gives you full control on where to store your tokens.

Note: Some storage providers are platform specific (e.g react-native) read more here.

Storage API

To create your own storage providers you need to implement three very simple methods:

storeToken(token) => Promise()

Stores the token and returns nothing.


forgetToken() => Promise()

Removes the token from storage and returns nothing.


getToken() => Promise(token)

Returns the token or null in case there is no token stored.


The MemoryStorage is the simplest implementation and can be used as reference:

export const createMemoryStorage = () => {
  const storage = {};

  return {
    storeToken: token => {
      storage.token = token;
      return Promise.resolve();
    },
    forgetToken: () => {
      delete storage.token;
      return Promise.resolve();
    },
    getToken: () => Promise.resolve(storage.token),
  };
};

Available Storage

  • MemoryStorage - keeps the token on in-memory variable.
  • AsyncStorage - store the token in react-native's AsyncStorage
2.2.9

3 years ago

2.2.7

3 years ago

2.2.8

3 years ago

2.2.6

4 years ago

2.2.5

4 years ago

2.2.4

4 years ago

2.2.3

4 years ago

2.2.2

4 years ago

2.2.1

4 years ago

2.2.0

4 years ago

2.1.0

4 years ago

2.0.0

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.9

4 years ago

1.0.8

4 years ago

1.0.7

4 years ago

1.0.6

4 years ago

1.0.5

4 years ago

1.0.4

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.3

4 years ago

1.0.0

4 years ago