productive-auth v2.2.9
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
- Login
- Two-Factor Login
- Passwordless Login
- Logout
- Refresh Token
- Forgot Password
- Login With 3rd Party
- Enable Two-Factor Authentication
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
tocreateClient
.
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
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago