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.

Typescript
<ProtectedRoute path='/securedRaDec' component={SecuredRaDecInput} />

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.