Logging into the app with Okta

Step 4 of 666%

Logging into the app with Okta

With the Okta, Box, and basic application set up, we can turn our attention to the first step in the application code flow, the Okta login.

During the Okta login we will employ the OpenID Connect (OIDC) frameworks of the language used to redirect the user to Okta to log in and pass Okta user information back to the application. Those Okta user details will in turn be used to validate and create Box users in the next step.

This section will walk you through:

  • Setting up the application configuration skeleton.
  • Defining the routes for the chosen framework to handle user traffic.
  • Passing Okta user information to the next Box user validation step.

Set up the Skeleton

In your local application directory, load the server.js file created in step 1.

Start by copying the following package definitions and configuration information into the file.

const session = require('express-session');
const { ExpressOIDC } = require('@okta/oidc-middleware');
const bodyParser = require('body-parser');
const boxSDK = require('box-node-sdk');
const config = require('./config.js');
const express = require('express')();
const http = require('http');
const path = require('path');
const fs = require('fs');

express.use(session({
  secret: 'this should be secure',
  resave: true,
  saveUninitialized: false
}));

const oidc = new ExpressOIDC({
  issuer: `https://${config.oktaOrgUrl}/oauth2/default`,
  client_id: config.oktaClientId,
  client_secret: config.oktaClientSecret,
  appBaseUrl: config.oktaBaseUrl,
  loginRedirectUri: `${config.oktaBaseUrl}${config.oktaRedirect}`,
  scope: 'openid profile'
});

express.use(oidc.router);
express.use(bodyParser.json());
express.use(bodyParser.urlencoded({
  extended: true
}));

This sets up the Express configuration and Okta OIDC connector information. Express is set to use the OIDC connector and the Okta information that we saved in step 2 of this quick start is used to configure the connector for our Okta integration.

Now add the routing details.

// Redirect to Okta login
express.get('/', (req, res) => {
  // TODO: HANDLE ROUTE
});

This defines the entry route for our application. When a user attempts to visit our application root (/) the code within this route will be run.

Lastly, add the Express server initialization to listen for traffic.

// Create server
const port = process.env.PORT || 3000;
http.createServer(express).listen(port, () => {
  console.log(`Server started: Listening on port ${port}`);
});

In your local application directory, load the /src/main/java/com/box/sample/Application.java file created in step 1, or similar directory if an alternate application name was used.

Copy the following basic application structure into the file.

package com.box.okta.sample;

import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.box.sdk.BoxAPIRequest;
import com.box.sdk.BoxConfig;
import com.box.sdk.BoxDeveloperEditionAPIConnection;
import com.box.sdk.BoxJSONResponse;
import com.box.sdk.BoxUser;
import com.box.sdk.CreateUserParams;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;

@RestController
@EnableAutoConfiguration
public class Application {
  static BoxDeveloperEditionAPIConnection api;

  // TODO: SET ROUTE

  // TODO: INITIALIZE SERVER
}

This sets up the needed imports, the Application class, and a standard shared Box API connection attribute, to be defined in the next step.

Replace // TODO: SET ROUTE with the following.

@RequestMapping("/")
String home(@AuthenticationPrincipal OidcUser user) throws IOException {
  // TODO: HANDLE ROUTE
}

The route mapping defines the entry route for our application. When a user attempts to visit our application root (/) in a logged out state, the OIDC connector will automatically push them through the Okta login, so we don't need to setup a redirect. When the user is in a logged in state, the code within this route will be run.

Replace // TODO: INITIALIZE SERVER with the following to initialize the Spring Boot server to listen for traffic.

public static void main(String[] args) {
  SpringApplication.run(Application.class, args);
}

In your local application directory, load the server.py file created in step 1.

Copy the following basic application structure into the file.

from flask import Flask, redirect, g, url_for
from flask_oidc import OpenIDConnect
from okta import UsersClient
from boxsdk import Client
from boxsdk import JWTAuth
import requests
import config
import json

app = Flask(__name__)
app.config.update({
  'SECRET_KEY': config.okta_client_secret,
  'OIDC_CLIENT_SECRETS': './client_secrets.json',
  'OIDC_DEBUG': True,
  'OIDC_ID_TOKEN_COOKIE_SECURE': False,
  'OIDC_SCOPES': ["openid", "profile"],
  'OIDC_CALLBACK_ROUTE': config.okta_callback_route
})

oidc = OpenIDConnect(app)
okta_client = UsersClient(config.okta_org_url, config.okta_auth_token)

This sets up the Flask configuration, Okta client, and Okta OIDC connector information. Flask is set to use the OIDC connector and the Okta information that we saved in step 2 of this quick start is used to configure the connector for our Okta integration.

Next, add a before_request definition to be run before route handling is engaged. We'll be using this to capture our Okta user information, if available.

# Fetch Okta user record if logged in
@app.before_request
def before_request():
  # TODO: HANDLE BEFORE REQUEST

Lastly, define the entry route for our application, as well as a box_auth route.

# Main application route
@app.route('/')
def start():
  # TODO: HANDLE MAIN ROUTE

# Box user verification
@app.route("/box_auth")
@oidc.require_login
def box_auth():
  # TODO: HANDLE BOX AUTH ROUTE

return 'Complete'

When a user attempts to visit our application root (/) the code within this route will be run. When we validate an Okta user, the code within the box_auth route will be run.

In your local application, load Views > Shared > Layout.cshtml. Once the ASP.NET application loads this will be the visual component that is rendered. At the top of the page, insert the following.

@using System.Security.Claims;

@if (User.Identity.IsAuthenticated)
{
    <p class="navbar-text">Hello, @User.Identity.Name</p>
}
else
{
    <a asp-controller="Account" asp-action="SignIn">Sign In</a>
}

If a user who is logged in to Okta visits, they will see the hello message. If the user is not logged in a sign in link will be provided to them.

Within the line <a asp-controller="Account" asp-action="SignIn">Sign In</a>, asp-controller="Account" means that the to be created Account controller will handle the request, and asp-action="SignIn" states that the SignIn method in that controller will be enacted. Save and close that file.

Within the Controllers directory create a new file, AccountController.cs. This will be the controller that is enacted when that sign in link is clicked.

Copy the following basic application structure into the file.

using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Okta.AspNetCore;
using Box.V2;
using Box.V2.Config;
using Box.V2.JWTAuth;
using Box.V2.Models;

public class AccountController : Controller
{
    public IActionResult SignIn()
    {
        if (!HttpContext.User.Identity.IsAuthenticated)
        {
            return Challenge(OktaDefaults.MvcAuthenticationScheme);
        }

        return RedirectToAction("Profile", "Account");
    }

    [Authorize]
    [Route("~/profile")]
    public IActionResult Profile()
    {
        // TODO: HANDLE ROUTE
    }
}

When the user clicks on the sign in link the SignIn method in this controller will be run. If the user is not already authenticated they will be sent to Challenge, which will redirect the user to Okta to log in. This functionality is handled by the routing framework and does not require any additional code to enact. If the user is authenticated, they will be redirected to the Profile routing method.

Incomplete previous step

Please select a preferred language / framework in step 1 to get started.

Setup Application Route

We now need to define the code that will run when our main route (/) is engaged.

Replace // TODO: HANDLE ROUTE in the main route with the following code.

if (req.userContext && req.userContext.userinfo) {
  const tokenSet = req.userContext.tokens;
  const userInfo = req.userContext.userinfo;

  // If Okta ID is present, pass to Box user validation
  if (userInfo.sub) {
    box.validateUser(userInfo, res);
  } else {
    console.log('No Okta ID identified');
  }
} else {
  res.redirect('/login');
}

What we are doing in the above is first checking to see if there is any Okta user information available from the OIDC connector. When a user logs in the connector will make the Okta user and configuration information available to our route within req.userContext.

If user information is present, meaning the user is logged in to Okta, we pass the user information to box.validateUser along with the Express response object to see if there is an associated Box user available, which we'll define in the next step.

If no user information is present, we redirect the user to /login. The OIDC connector will automatically handle this route and force the user through to the Okta login.

Replace // TODO: HANDLE ROUTE in the main route with the following code.

// Validate OIDC user against Box
return validateUser(user);

The Java OIDC connector handles most of the heavy lifting for us. When a logged out user accesses this route they will automatically be pushed to the Okta login. Once logged in, an OIDC user object will be made available to the route. We pass that user object to a validateUser function, which we'll define in the next step.

Replace // TODO: HANDLE BEFORE REQUEST in the main route with the following code.

if oidc.user_loggedin:
  g.user = okta_client.get_user(oidc.user_getfield('sub'))
else:
  g.user = None

This will check if an OIDC user is available, meaning that the user has already logged in to Okta. If available we set a user object using the Okta client object, and if not we set the user object to None.

Next, replace // TODO: HANDLE ROUTE in the main route with the following code.

return redirect(url_for(".box_auth"))

When this code is engaged, if the user is not logged in to Okta they will be redirected to Okta to log in by the OIDC connector. After login, or if the user is already logged in, they will then be forwarded to the box_auth route code.

Finally, replace // TODO: HANDLE BOX AUTH ROUTE in the box_auth route with the following code.

box = Box();
return box.validateUser(g)

This creates a new instance of the Box class and calls the validateUser method, passing in the Okta user object. We'll define this class and methods in the next step.

Replace // TODO: HANDLE ROUTE in the main route with the following code.

var subClaim = HttpContext.User.Claims.First(c => c.Type == "sub");
var sub = subClaim.Value;

var nameClaim = HttpContext.User.Claims.First(c => c.Type == "name");
var name = nameClaim.Value;

Task userSearch = validateUser(name, sub);

Task.WaitAll(userSearch);

return Content(name);

This block will capture the Okta user account sub (unique ID) and name, then sends that data to the to be created validateUser method to find a matching Box user, which will be created in the next step.

Incomplete previous step

Please select a preferred language / framework in step 1 to get started.

Summary

  • You set up the skeleton routes and configuration for Okta.
  • You set up the main route handler to pass to Box user verification.