Adding Authentication
Add protected route in backend
To demonstrate authorization, we will need to create a “protected” route, that is, an endpoint that requires a valid authorization token to access.
Add new route with protection
We will add a new route to our server which is protected. To access this route, the request should contain a token containing the role esw-user
. We have set up some sample users when we start csw-services
with the Authentication and Authorization Service enabled, and we will use one of these users for our tutorial.
Add the following route below to SampleRoute.scala
. Note it requires the user to have the esw-user
role to access the endpoint. If you deleted the tilde (~) at the end of your route in the last tutorial, be sure to put it back, and then append the following:
- Scala
-
path("securedRaDecValues") { post { securityDirectives.sPost(RealmRolePolicy("Esw-user")) { _ => entity(as[RaDecRequest]) { raDecRequest => complete(raDecService.raDecToString(raDecRequest)) } } } }
Consume protected route in frontend
Now, we will create a component in our frontend UI that uses our protected route.
Add secured Fetch
Add the following method in api.ts
, which sends a request to our /securedRaValues
backend route.
- Typescript
-
export const securedPostRaDecValues = async ( baseUrl: string, raDecRequest: RaDecRequest, token: string ): Promise<RaDecResponse | undefined> => ( await post<RaDecRequest, RaDecResponse>( baseUrl + 'securedRaDecValues', raDecRequest, { Authorization: `Bearer ${token}` } ) ).parsedBody
Note that this method requires a token, which is then passed to the server with the request.
Create a React component to consume our secured route
In the pages
folder, create a file named SecuredRaDecInput.tsx
. Then create a SecuredRaDecInput
React component with the following form.
- Typescript
-
export const SecuredRaDecInput = (): JSX.Element => { return ( <Form onFinish={onFinish} style={{ padding: '1rem' }} wrapperCol={{ span: 1 }}> <Form.Item label='RaInDecimals (secured)' name='raInDecimals'> <Input role='RaInDecimals' style={{ marginLeft: '0.5rem' }} /> </Form.Item> <Form.Item label='DecInDecimals (secured)' name='decInDecimals'> <Input role='DecInDecimals' /> </Form.Item> <Form.Item> <Button type='primary' htmlType='submit' role='Submit'> Submit </Button> </Form.Item> </Form> ) }
Use secured fetch in our component
Again, a reference to the Location Service is obtained via a context named LocationServiceProvider
. Since this component requires authorization, we use another context to get a reference to the authorization system.
Add the following as first lines inside the SecuredRaDecInput
component.
- Typescript
-
export const SecuredRaDecInput = (): JSX.Element => { const locationService = useLocationService() const { auth } = useAuth()
The useAuth
method is a hook provided in hooks/useAuth.tsx
which accesses the context. Like LocationServiceProvider
, this context, AuthContextProvider
is made available to the component during construction in App.tsx
.
Now, add an onFinish
handler above the return statement, similar to our non-secured component. Note this time we will obtain the token from the authorization context and pass that to our API method.
- Typescript
-
const onFinish = async (values: RaDecRequest) => { const backendUrl = await getBackendUrl(locationService) const valueInDecimal = { raInDecimals: Number(values.raInDecimals), decInDecimals: Number(values.decInDecimals) } if (backendUrl) { const token = auth?.token() if (!token) { errorMessage('Failed to greet user: Unauthenticated request') } else { const response = await securedPostRaDecValues( backendUrl, valueInDecimal, token ) if (response?.formattedRa && response?.formattedDec) { console.log(response.formattedRa) console.log(response.formattedDec) } else { console.error(response) throw new Error( 'Invalid response, formattedRa or formattedDec field is missing' ) } } } }
Add the necessary imports.
Connect our new component
Next, we will add the protected route in Routes.tsx
within the <Switch>
block.
Add an action for our new route in MenuBar.tsx
below previously added RaDec Menu.Item
- Typescript
-
<Menu mode='horizontal'> <Menu.Item key='raDec'> <Link to='/'>RaDec</Link> </Menu.Item> <Menu.Item key='securedRaDec'> <Link to='/securedRaDec'>SecuredRaDec</Link> </Menu.Item> </Menu>
Add Login & Logout functionality
To provide login and logout capabilities, we will make use of the generated Login
and Logout
components.
Add menu item actions for logging in and logging out in MenuBar.tsx
below the previously added SecuredRaDec Menu.Item
The menu item will change depending on whether the user is logged in or not.
- Typescript
-
<Menu.Item key='securedRaDec'> <Link to='/securedRaDec'>SecuredRaDec</Link> </Menu.Item> {isAuthenticated ? <Logout logout={logout} /> : <Login login={login} />}
Note the authorization hook is used again here to get a handle to the authorization store.
- Typescript
-
export const MenuBar = (): JSX.Element => { const { auth, login, logout } = useAuth() const isAuthenticated = auth?.isAuthenticated() ?? false
Try it out
Compile the backend and restart it. Then run the UI as before and try it out. Clicking on the SecuredRaDec
menu item will take you to the login page. Be sure to login with theesw-user1
user with the password esw-user1
. Once logged in, you will be able to use this form. The behavior is the same as the non-secured version, but it gives you the idea of how pages and routes can be protected. You will have to switch to the RaDec
tab to see your inputs.