Rent the Runway - rtr-react-okta-auth
-
Rent the Runway - rtr-react-okta-auth
- Install
- What?
- Scope
-
API and Components
- API
-
Component Summary
- Locking out Routes based on Groups
- Locking out JSX based on Groups
- Locking out Routes based on Claims
- Locking out JSX based on Claims
- Locking out JSX based on application specific logic such as permissions / RBAC
- Waiting for authorization state to be known
- Locking out Routes based on application specific logic such as permissions / RBAC
- useWhenAuthenticatedAnd() Custom Hook (alias useWhen())
- App Setup
- Okta Setup Summary
- Detailed Okta Setup
Install
npm install @rent-the-runway/rtr-react-okta-auth @okta/okta-react@5.1.2
-or-
yarn add @rent-the-runway/rtr-react-okta-auth @okta/okta-react@5.1.2
What?
A library that allows a React application to interact with Okta. It expands on the functionality of okta-react.
okta-react
is detailed by example here however it only handles authentication, it does not provide for authorization.
@rent-the-runway/rtr-react-okta-auth
provides for group-based authorization.
Specifically, access to Routes and JSX can be permitted to users
- who are members of particular Okta user groups or additionally
- who have specific claims or
- any other arbitrary condition such as application specific RBAC
The library can be used for both JavaScript and TypeScript.
More okta-react
version 5.1.2 documentation here:
Scope
This library is concerned with Okta's implicit flow. It's intent is to lock down UI fragments and react <Route />
's client side, i.e. within the browser.
This library is not directly concerned with securing server side API end points however it can assist in doing so.
If an API end point needs to be made secure it is the responsibility of the individual developers to make it so.
More information on this here and here
API and Components
API
const {
isMemberOf,
isMemberOfAll,
isMemberOfAny,
hasClaim,
hasAllClaims,
hasAnyClaims,
authorizationStateKnown,
authCtx,
} = useRtrOktaAuth();
Pretty much all that you need is the hook
useRtrOktaAuth()
This gives you everything you need to lockdown components/JSX and routes.
Okta Groups:
-
isMemberOf('vampires'): boolean
- returns true if the user is a member of the specified group. -
isMemberOfAll(['vampires', 'werewolves']): boolean
- returns true if the user is a member ofAll
specified groups. -
isMemberOfAny(['vampires', 'werewolves']): boolean
- returns true if the user is a member ofAny
specified groups.
Okta Claims
-
hasClaim('CanDoA'): boolean
- returns true if the user has the specified Claim. -
isMemberOfAll(['vampires', 'werewolves']): boolean
- returns true if the user hasAll
specified claims. -
isMemberOfAny(['vampires', 'werewolves']): boolean
- returns true if the user hasAny
specified claims. -
authCtx
is the nativeokta-react' hook i.e.:
import { useOktaAuth } from '@okta/okta-react';
authorizationStateKnown
-
authorizationStateKnown
- OnceauthContext.isAuthenticated
becomestrue
this library makes a call to Okta to fetch the user object. It is only when the user has been acquired that we can determine which groups etc. a user belongs to. Once the user has been acquiredauthorizationStateKnown
becomestrue
. You may in fact want to consider not rendering the content of your app until this is known. This will avoid any flicker as components appear; example:
import * as React from 'react';
import { Route } from 'react-router';
import {
RouteWhenMemberOf,
RouteWhenMemberOfAny,
useRtrOktaAuth,
} from '@rent-the-runway/rtr-react-okta-auth';
import AdminBlog from './pages/AdminBlog';
import Home from './pages/Home';
import ReadBlog from './pages/ReadBlog';
interface Props {}
const AppRtrOktaAware: React.FC<Props> = () => {
const { authorizationStateKnown } = useRtrOktaAuth();
if (!authorizationStateKnown) return null;
return (
<>
<Route path="/" component={Home} exact />
<RouteWhenMemberOf
group="blog_Admin"
component={AdminBlog}
path="/admin"
exact
/>
<RouteWhenMemberOfAny
groups={['blog_read', 'blog_Admin']}
component={ReadBlog}
path="/read"
exact
/>
</>
);
};
export default AppRtrOktaAware;
That entire API will allow you do all that you need but for convenience and to save lots of boiler plate the following components are available:
Component Summary
Locking out Routes based on Groups
<RouteWhenMemberOf
group="admin"
path="/orders"
exact={true}
component={Standard}
unauthenticatedComponent={Unauthenticated} //optional
unauthorizedComponent={Unauthorized} //optional
/>
Authenticated users from the group "admin" will have access to /orders
<RouteWhenMemberOfAny
groups={["standard", "admin"]}
path="/orders"
exact={true}
component={Standard}
unauthenticatedComponent={Unauthenticated} //optional
unauthorizedComponent={Unauthorized} //optional
/>
Authenticated users from groups "standard" OR "admin" will have access to /orders
<RouteWhenMemberOfAll
groups={["standard", "admin"]}
path="/orders"
exact={true}
component={Standard}
unauthenticatedComponent={Unauthenticated} //optional
unauthorizedComponent={Unauthorized} //optional
/>
Authenticated users from groups "standard" AND "admin" will have access to /orders
If the user arrives unauthenticated at one of those <Route />
's either a default Unauthenticated page or the specified unauthenticatedComponent
will render.
If the user arrives authenticated but not authorized (based on groups or claims) at one of those <Route />
's either the default Unauthorized page or the specified unauthorizedComponent
will render.
Redirect for Unauthenticated or Unauthorized Route Access
The unauthenticatedComponent
or unauthorizedComponent
components can simply return a <Redirect />
component, e.g.
const ToUnauthorized = function() {
return <Redirect to="/unauthorized" />;
}
Locking out JSX based on Groups
<WhenMemberOf group="admin">
<div>Rendered only when the user is authenticated and is a member of "admin"</div>
</WhenMemberOf>
<WhenMemberOfAny groups={["standard", "admin"]}>
<div>Rendered only when the user is authenticated and is a member of "standard" OR "admin"</div>
</WhenMemberOfAny>
<WhenMemberOfAll groups={["standard", "admin"]}>
<div>Rendered only when the user is authenticated and is a member of "standard" AND "admin"</div>
</WhenMemberOfAll>
The inverse of each is also available
<WhenNotMemberOf group="admin">
<div>Rendered only when the user not authenticated or is not a member of "admin"</div>
</WhenNotMemberOf>
<WhenNotMemberOfAny groups={["standard", "admin"]}>
<div>Rendered only when the user is not authenticated or is not a member of "standard" OR "admin"</div>
</WhenNotMemberOfAny>
<WhenNotMemberOfAll groups={["standard", "admin"]}>
<div>Rendered only when the user is not authenticated or is not a member of "standard" AND "admin"</div>
</WhenNotMemberOfAll>
Locking out Routes based on Claims
<RouteWhenHasClaim
claim={"CanDoA"}
path="/orders"
exact={true}
component={Admin}
unauthenticatedComponent={Unauthenticated} //optional
unauthorizedComponent={Unauthorized} //optional
/>
Authenticated users with claim "CanDoA" will have access to /orders
<RouteWhenHasAnyClaims
claims={["CanDoA", "CanDoB"]}
path="/orders"
exact={true}
component={Admin}
unauthenticatedComponent={Unauthenticated} //optional
unauthorizedComponent={Unauthorized} //optional
/>
Authenticated users with claims "CanDoA" OR "CanDoB" will have access to /orders
<RouteWhenHasAllClaims
claims={["CanDoA", "CanDoB"]}
path="/orders"
exact={true}
component={Admin}
/>
Authenticated users with claims "CanDoA" AND "CanDoB" will have access to /orders
Locking out JSX based on Claims
<WhenHasClaim claim="CanDoA">
<div>Will be rendered only when the user is authenticated and has a claim called "CanDoA"</div>
</WhenHasClaim>
<WhenHasAnyClaims claims={["CanDoA", "CanDoB"]}>
<div>Will be rendered only when the user is authenticated and has claims called "CanDoA" OR "CanDoB"</div>
</WhenHasClaims>
<WhenHasAllClaims claims={["CanDoA", "CanDoB"]}>
<div>Will be rendered only when the user is authenticated and has claims called "CanDoA" AND "CanDoB"</div>
</WhenHasAllClaims>
The inverse of each is also available
<WhenNotHasClaim claim="CanDoA">
<div>Will be rendered only when the user is not authenticated or not has a claim called "CanDoA"</div>
</WhenNotHasClaim>
<WhenNotHasAnyClaims claims={["CanDoA", "CanDoB"]}>
<div>Will be rendered only when the user is not authenticated or not has claims called "CanDoA" OR "CanDoB"</div>
</WhenHasClaims>
<WhenNotHasAllClaims claims={["CanDoA", "CanDoB"]}>
<div>Will be rendered only when the user is not authenticated or not has claims called "CanDoA" AND "CanDoB"</div>
</WhenNotHasAllClaims>
Locking out JSX based on application specific logic such as permissions / RBAC
Note Okta does not inherently provide any means to achieve RBAC. Permission management must take place in the application code.
With that in mind, this library provide a generic means of locking out Routes and JSX. The following components are authentication aware.
<WhenAuthenticatedAnd isTrue={() => hasPermission(permissions.canViewOrder)}>
<li className="nav-item">
<Link to="/view-order" className={orderClazz}>
canViewOrder
</Link>
</li>
</WhenAuthenticatedAnd>
Alias <When />
The above example shows how RBAC can be achieved. hasPermission
is application code and has nothing to do with this library.
However, hasPermission
need not be concerned with authentication. It needs to be concerned with authorization only. This is because isTrue
will first check authentication before invoking hasPermission()
. If not authenticated isTrue
returns false. hasPermission()
must return a boolean.
Waiting for authorization state to be known
<WhenAuthStatePending>
We are just waiting to see if the user is authorized or not, 1 moment...
</WhenAuthStatePending>
So in theory you can then combine components
<WhenAuthStatePending>
<Spin />
</WhenAuthStatePending>
<WhenMemberOf group="admins">
<button onClick={doStuff}>Off We go</button>
</WhenMemberOf>
<WhenNotMemberOf group="admins">
<button disabled>Off We go</button>
</WhenNotMemberOf>
Locking out Routes based on application specific logic such as permissions / RBAC
<RouteWhenAuthenticatedAnd
isTrue={() => hasPermission(permissions.canViewOrder)}
path="/view-order"
exact={true}
component={ViewOrder}
/>
Alias <RouteWhen />
Here the isTrue
will check authentication before invoking hasPermission()
. If not authenticated isTrue
returns false. hasPermission()
must return a boolean.
<RouteWhen />
will redirect to the Okta login page is isTrue
returns false.
As with the other <RouteWhenXyZ />
components, <RouteWhen />
takes an optional unauthorizedComponent
and unauthenticatedComponent
parameter (Which can render a <Redirect />
if so desired>).
useWhenAuthenticatedAnd() Custom Hook (alias useWhen())
If we want to do something like disable a button based on some criteria the useWhen
custom hook can be used.
<button
disabled={!canIssueRefund}
className="btn btn-primary"
onClick={issueRefund}
>
Refund
</button>
const { when } = useWhenAuthenticatedAnd();
const { userGroups } = useRtrOktaAuth();
const canIssueRefund = canRefund();
function canRefund() {
return when(() => hasPermission(permissions.canRefund));
function hasPermission(permission: string) {
const permissions = getPermissions(userGroups);
return permissions.includes(permission);
}
}
In this example, hasPermission
and canRefund
are arbitrary application code. Nothing to do with this library.
when()
i.e. return when(() => hasPermission(permissions.canRefund));
is authentication aware and checks the authentication state before invoking the function argument. If not authenticated the function argument is not invoked. when(...)
simply returns false. Otherwise it returns the result of the function parameter (which must return a boolean)
canRefund()
could in this case just return direct from hasPermission(permissions.canRefund)
. when(...)
might seem a little redundant but the key point is that when()
is authentication aware and factors that into the result. i.e. if not authenticated, false
is returned.
Note The reason for the generic arbitrary nature of...
<WhenAuthenticatedAnd />
<RouteWhenAuthenticatedAnd />
and
useWhen()
...is to accommodate such things as RBAC. The application can manually match permissions to Okta groups. These components are authentication state aware.
App Setup
There is a tiny bit of setup required, additional the the okta-react
setup.
Begin with the standard okta-react
setup.
<Router>
<Security oktaAuth={oktaConfig} restoreOriginalUri={restoreOriginalUri}>
<AppOktaAware />
<Route path="/login/callback" component={LoginCallback} />
</Security>
</Router>
Note the <AppOktaAware />
component. You need to create this yourself.
import { useOktaAuth } from '@okta/okta-react';
import * as React from 'react';
import { RtrOktaAuth } from '@rent-the-runway/rtr-react-okta-auth';
import AppRtrOktaAware from './AppRtrOktaAware';
const AppOktaAware: React.FC = () => {
const authCtx = useOktaAuth();
return (
<RtrOktaAuth authCtx={authCtx}>
<AppRtrOktaAware />
</RtrOktaAuth>
);
};
export default AppOktaAware;
It must pass an instance of useOktaAuth()
into <RtrOktaAuth />
which provides Context
for all the rtr-react-okta-auth
components.
<AppRtrOktaAware />
is pretty much your App
which is now Okta aware so you can use all of the components this library offers.
Example:
import * as React from 'react';
import { Route } from 'react-router';
import { RouteWhenMemberOf, RouteWhenMemberOfAny } from '@rent-the-runway/rtr-react-okta-auth';
import AdminBlog from './pages/AdminBlog';
import Home from './pages/Home';
import ReadBlog from './pages/ReadBlog';
const AppRtrOktaAware: React.FC = () => {
return (
<>
<Route path="/" component={Home} exact />
<RouteWhenMemberOf
group="blog_Admin"
component={AdminBlog}
path="/admin"
exact
/>
<RouteWhenMemberOfAny
groups={['blog_read', 'blog_Admin']}
component={ReadBlog}
path="/read"
exact
/>
</>
);
};
export default AppRtrOktaAware;
That's all there is to it.
Okta Setup Summary
Setup your own Okta account as explained here.
You simply need to create Groups
or Claims
in Okta and add users to them. This documentation assumes two groups named admin
and standard
.
To get Okta to return the groups, you need to add a claim that returns them. There are different ways to do this. A simple way is to:
Groups
- Don't create a new scope
- Create a new claim as so:
- Name:
groups
- Include in token type:
ID Token
,Always
- Value type:
Groups
- Filter:
Matches regex
.*
- Include in:
Any scope
- Name:
- Save
- More detail below
Claims
- Create a new Claim giving it a name e.g.
CanDoA
- Include in the "ID Token"
Always
- Value Type
Groups
- Filter "Equals" <the-group-name>
- Include in
Any Scope
or in a designated scope - Now these claims will be present in the
user
object (const { user } = useContext(AuthContext)
details below )
Detailed Okta Setup
Setting up the Okta Application
Assuming access to an Okta account (you can create a dev account for free by signing up with Okta).
Within the Okta dashboard, click on the 'Applications' link in the main navigation menu.
- Click the 'Create App Integration' button.
- Choose OIDC - OpenID Connect.
- Then Choose the 'Single Page App' option and hit Next
For 'Grant type' check the 'Implicit' option and also check the 'Authorization Code' option
- Give it a suitable name (Example SPA for now).
- Update the Sign-in redirect URI to http://localhost:**3000**/login/callback
- Update the Sign-out redirect URIs also to http://localhost:**3000**
- Optionally, add URI's for the different environments e.g. dev, staging, prod
- Trusted Origins Base URIs is generally required if you want to be able to logout. Add http://localhost:3000.
Hit Save
Note or copy the Client ID for later.
Navigate into 'Security` -> 'API' -> 'Authorization Servers' from the main navigation menu
In the 'Trusted Origins' tab add the various URL's that will need access to the Okta application. This is necessary in order to facilitate CORS.
Getting Okta to supply user groups
Back on the 'Authorization Servers' tab click on 'default' to configure it.
In the 'Claims' tab, add a claim with the following values.
Do this twice. Once for the ID Token and once for the Access Token.
This will add a default claim called 'groups' that will return a list of Okta user groups for which the user is a member.
This is not the only way to achieve this but in order to work with the @rent-the-runway/rtr-react-okta-auth
the claim groups
should exist and its value should be the group names.
In the 'Access Policies' tab, enable access policy by adding a rule and configuring accordingly
Now const { user } = useRtrOktaAuth();
will include a groups
property.
Getting Okta to supply Claims
In the Okta admin we can create a claim and associate it with a user-group as so.
To associate a Claim with more than one group we can use regular expressions
Now const { user } = useRtrOktaAuth();
will include each claim as an individual property.
While the code for this library keeps groups
and claims
separate, setting Okta up this way makes groups
feel like roles
and claims
feel like permissions
within those roles
.