React - way to ensure I am authenticated?

nodejs
reactjs

#1

Hi, when the token is expired It’s still getting all the way thru to auth.userinfo() and then failing.

Is there some kind of method available for the okta-react library which will allow me to check whether the user is authenticated?

  // get accessToken, if none available, logout
  const accessToken = await this.props.auth.getAccessToken();
  if (!accessToken) { this.props.auth.logout() }

  console.log("getAccount", "accessToken")

  // try to generate userinfo
  try {
    let userinfo = await this.props.auth.getUser(accessToken);
    console.log("getAccount", "userinfo", userinfo)

I assumed accessToken will be empty if I can’t get one so fire props.auth.logout() if it is, that doesn’t seem to work.

Any ideas what I’m doing wrong?

Thanks in advance,


#2

With the this.props.auth.logout() method, will that force a logout even if the access token has expired?


#3
 async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      this.setState({ authenticated });
    }
  }

This code is from: https://developer.okta.com/quickstart/#/react/nodejs/generic

Does this work for you?


#4

That works fine, and what I used as the basis for my app’s checkAuth method so I could also pull in my local user profile record.

The strange thing is that 99% of the time it’s good but then if you go back to it after the token has expired I’m able to fire this.props.auth.logout() but it doesn’t trigger the logout, the page kind of freezes in limbo.

Checking the console, the error occured inside the SDK running:

https://dev-754086.oktapreview.com/oauth2/default/v1/userinfo

With a response of:

Response headers
HTTP/1.1 401 Unauthorized
Date: Tue, 13 Feb 2018 01:39:46 GMT
Server: nginx
Public-Key-Pins-Report-Only: pin-sha256="jZomPEBSDXoipA9un78hKRIeN/+U4ZteRaiX8YpWfqc="; pin-sha256="axSbM6RQ+19oXxudaOTdwXJbSr6f7AahxbDHFy3p8s8="; pin-sha256="SE4qe2vdD9tAegPwO79rMnZyhHvqj3i5g1c2HkyGUNE="; pin-sha256="ylP0lMLMvBaiHn0ihLxHjzvlPVQNoyQ+rMiaj0da/Pw="; max-age=60; report-uri="https://okta.report-uri.io/r/default/hpkp/reportOnly"
Content-Type: application/json;charset=UTF-8
ADRUM_0: g:660f21de-3bb9-46e2-ba37-292b9211a86c
ADRUM_1: n:Okta_6d5b1e30-d05a-4894-a37b-81b5f6c60e0e
ADRUM_2: i:11555
ADRUM_3: e:54
X-Okta-Request-Id: WoJB4s7GcDfYdAOfu6pfuQAAA9E
P3P: CP="HONK"
X-Rate-Limit-Limit: 10000
X-Rate-Limit-Remaining: 9996
X-Rate-Limit-Reset: 1518486009
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type
Cache-Control: no-cache, no-store
Pragma: no-cache
Expires: 0
Access-Control-Expose-Headers: WWW-Authenticate
WWW-Authenticate: Bearer error="invalid_token", error_description="The token has expired."
Set-Cookie: JSESSIONID=33B3047A2E91E1F4870376CD7B286B17; Path=/
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked

Maybe the props is empty and it’s can’t fully fire auth.logout() via:

  } catch (err) {
    console.log("getAccount", "logout")
    this.props.auth.logout()
  }

To get around I could trigger a full refresh with window.location.reload() but I can’t see a way to add a callback to that response.

Any ideas?


#5

I could also manually check the expire time in okta_token_storage stored in the browser’s localStorage and if it’s expired just go straight to triggering a logout?

May well put the logged in area inside it’s own route http://localhost:8080/admin rather than have a secure page in http://localhost:8080

Sorry, clutching at straw’s trying to figure this edge case out.


#6

huh…

Looking at the code for the react library, I’m unsure what is going on here. I don’t see the path where an expired token would end up calling user info.

I just cranked down my access token timeout to see if I can reproduce, but while I’m checking, do you have sample code/project that can illustrate the problem?


#7

Thanks Tom, here’s the code from home, getAccount in helpers and a segment of the app.js

// app.js

function customAuthHandler({ history }) {
  history.push(`/`);
}

class App extends Component {

  render() {
    return (
      <Router history={history} i18n={ i18n }>
        <Security
          issuer={config.oidc.issuer}
          client_id={config.oidc.clientId}
          redirect_uri={config.oidc.redirectUri}
          onAuthRequired={customAuthHandler}
        >
          <Header i18n={ i18n }/>
          <div>
            <Route path="/implicit/callback" component={ImplicitCallback} />
            <Route exact path="/" component={Home} />
            <Route exact path="/signup" component={SignupPage} />

            <SecureRoute exact path="/lessons/all" component={ListLessons} />
          </div>
        </Security>
      </Router>
    );
  }
}

export default App;

/////////////////////

// home.js

import React from 'react';
import moment from 'moment';
import {Container, Row, Col, Badge} from 'reactstrap';
import { Link } from 'react-router-dom';
import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table';
import { withAuth } from '@okta/okta-react';
import { getAccount } from './../helpers';

import LoginPage from './okta/LoginPage';

export default withAuth(class Home extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      filteredData: [],
      resetDate: false,
      empty: [],
      hide: false,
      authenticated: null,
      userinfo: null,
      ready: false
    };
    this.getPage = this.getPage.bind(this);
    this.getAccount = getAccount.bind(this);
  }

  async componentDidMount() {
    await this.getAccount();
  }

  async componentDidUpdate() {
    await this.getAccount();
  }

  getPage() {
    if (this.state.ready) {
      switch (this.state.userinfo.role) {
        case 'teacher': {
          return (<DashTeacher userinfo={this.state.userinfo} />)
        }
        case 'admin': {
          return (<DashAdmin userinfo={this.state.userinfo} />)
        }
        default: {
          return (<DashTeacher userinfo={this.state.userinfo} />)
        }
      }
    } else {
      return (<div></div>)
    }
  }

  render() {
    return (
      <div>
        { this.state.authenticated !== null &&
          <Container>
            <Col lg={12}>
              { this.state.authenticated &&
                this.getPage()
              }
              { !this.state.authenticated &&
                (<LoginPage auth={this.props.auth} />)
              }
            </Col>
          </Container>
        }
      </div>
    )
  }
});

/////////////////////

// helpers.js

import history from './history';
import config from './.config';
import request from 'superagent';
import uuidv4 from 'uuid/v4';
import { translate } from 'react-i18next';

async function getUserProfile(id, accessToken) {
  return await request
    .get(`/v1/okta/sub/${id}`)
    .set({ Authorization: `Bearer ${accessToken}` })
    .set('accept', 'json')
    .then(function(res) {
      return res.body
  });
}

function verifyAccountRole(current_role, allowed) {
  try {
    const allowed_array = allowed.replace(/ +(?= )/g,'').split(' ')
    if (!allowed.includes(current_role)) {
      goTo('/') // if you don't have rights to see this page, go back to root url
    }
  } catch (err) {
    console.log(err, current_role, allowed)
  }
}

async function getAccount(roles = null) {
  const authenticated = await this.props.auth.isAuthenticated();
  // if authenticated does not equal this.state.authenticated
  if (authenticated !== this.state.authenticated) {
    // if authenticated and userinfo does not exist?
    if (authenticated && !this.state.userinfo) {

      console.log("getAccount", "authenticated", authenticated)

      // get accessToken, if none available, logout
      const accessToken = await this.props.auth.getAccessToken();
      if (!accessToken) { this.props.auth.logout() }

      console.log("getAccount", "accessToken", accessToken)

      // try to generate userinfo
      try {
        let userinfo = await this.props.auth.getUser(accessToken);
        console.log("getAccount", "userinfo", userinfo)

        let userProfile = await getUserProfile(userinfo.sub, accessToken)
        console.log("getAccount", "userProfile")

        userinfo.role = userProfile.role
        userinfo.user_id = userProfile._id
        userinfo.classrooms = userProfile.classrooms || []

        // with userinfo generated, save it to state
        this.setState({ authenticated, accessToken, userinfo, ready: true
        }, () => {
          console.log("getAccount", "setState")

          if (roles !== null) { // if role given, verify role
            verifyAccountRole(userinfo.role, roles);
          }
        })
      } catch (err) {
        console.log("getAccount", "logout")
        this.props.auth.logout()
        // window.location.href = '/'
      }

    } else {
      this.setState({ authenticated })
    }
  }
}

export {
  verifyAccountRole,
  getUserProfile,
  getAccount,
}

#8

Here’s the componentDidMount and componentDidUpdate in ListLessons to demonstrate what verifyRole does.

async componentDidMount() {
  await this.getAccount("admin instructor teacher");
  LessonActions.getAll(this.state.accessToken);
  LessonStore.on("change", this.getLessons);
}

async componentDidUpdate() {
  await this.getAccount("admin instructor teacher");
}

#9

Had a long think about it yesterday.

Think it may be something wrong with my userProfile Express Server method, will fix.

Also on the JWT Verifier in the Express app if it fails (using the example node code) it sends a 4040 so I might change that to a 422 that my client-side can pick up especially and trigger a logout.

That should solve it.