Bearer Token Authentication with wwRestProcess
Unlike standard wwProcess
classes, wwRestProcess
does not work with standard session cookies and by default all session support is turned off. However, you can enable session support via Bearer Token authentication which reads a user supplied identity token from the Authorization
HTTP header.
Before jumping into Bearer Token Authentication let's discuss traditional Session based Authentication for HTML applications using Cookies.
Authentication in HTML based Web Applications
For regular HTML page Web applications using MVC or raw HTML output Web Connection has support for some basic Authentication functionality via the wwUserSecurity
class, which is based on HTTP Cookies tied to a wwSession
record, which in turn maps to a user record in wwUserSecurity
. If your needs are simple, you can just use the user table and user access as is, or you can customize both the structure and the logic by subclassing wwUserSecurity
to handle how users are authenticated.
This is great for HTML based Web applications that can use cookies: You have a login form, you sign in, get assigned a cookie and the cookie then follows you around in subsequent requests in the browser, and the server can grab the cookie and map it to a session and/or a User record.
If you want more features you can also look at the West Wind User Security Manager which provides additional features for user signup, email validation, password recovery, and user management.
REST Service Authentication
REST Services however have other requirements in order to do security. Typically services can't use Cookies because clients often aren't browsers, or even if they are they might be connecting across different domains that can't persist a cookie.
REST applications tend to use Bearer Tokens which is basically an HTTP Header in a sepecific format that looks like this:
Authorization: Bearer <token>
Bearer tokens are issued by some sort of authentication process which can either be something simple as another API call to Signin
with username and password (or some other combination), or something more complicated like an oAuth service that provides the token externally.
In order to provide some base functionality for REST Service Authentication v7.35 introduces a couple of new wwRestProcess
class methods that can be used to provide Bearer Token
based Authentication:
InitTokenSession()
This method is the core feature that ties a generated token to a Web ConenctionwwSession
object via itscSessionId
parameter.InitTokenSession()
either retrieves an existing session from a providedBearer
token, or if one isn't provided or matched provides an empty session. To create a new Token you can have a custom sign in method and callNewSession()
to map your custom user/customer/etc. to a session with a session holding any additional data.Authenticate()
If you want basic mapping of the session to a user in a similar way to the way HTML authentication works with cookies you can use theAuthenticate()
method which serves a dual purpose for:- Validating a wwSession Token and Loading a User
- Authenticating user credentials
You can think of InitTokenSession()
as a low level implementation. If you simply want to generate a token and provide a simple mapping to a user id or other reference, then all you need is wwRestProcess::InitTokenSession()
.
If you want to map a user record that can work with wwUserSecurity
then you can use wwRestProcess::Authenticate()
, which uses the existing wwUserSecurity
infrastructure to map tokens to users and provides both the wwSession
and wwUserSecurity
and user to your process methods.
The Authenticate()
call builds ontop of the InitTokenSession()
functionality so both are needed in order to use Authenticate()
just like InitSession()
is required for Authenticate in HTML wwProcess
authentication.
Implementation: InitTokenSession()
You can use pure InitTokenSession()
session based authentication if your needs are very simple and you simply need to validate a user, and then only need to know if the user is logged in or not. If you don't need access to a user record or anything more complex than a mapping ID that you can use to map to your own business objects, using this approach is easiest and most light weight as it uses only a Session object and table.
InitTokenSession()
is used to initialize the wwSession
instance and guarantees that a session is loaded.
If a Bearer Token is found and can be mapped to a session that session becomes available in
THIS.oSession
.If there's no match an empty session that doesn't have any backing on disk yet is returned.
There are two steps to this process:
- Call
InitTokenSession()
inOnProcessInit()
- Create some a
Signin
method to create and return the Bearer Token to the client
The first step to use token handling via wwSession is to call InitTokenSession()
in OnProcessInit()
so it fires for every request:
FUNCTION OnProcessInit
Response.Encoding = "UTF8"
Request.lUtf8Encoding = .T.
...
*** Pick up existing token or create a new token
*** and set on the oSession object
THIS.InitTokenSession()
*** Define anonymous requests that don't need validation
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testpage","signin")
*** Fail if not authorized (ie. a new session has no or failed bearer token)
IF !llIgnoreLoginRequest AND this.oSession.lIsNewSession
THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
RETURN .F.
ENDIF
RETURN .T.
After InitTokenSession()
returns, the THIS.oSession
(or Session
inside of your Process methods) is available with either a matched session instance, or a new empty session.
You can check whether a session is valid by looking at THIS.oSession.lIsNewSession
. If .T.
the session is not mapped to an existing token, but a new empty session. If the session is empty you likely don't want to allow access to the application except to some specific requests that have to be anonymous - like the sigin
for example since you can't require an authorized user, before you have chance to sign in. ??
The code above shows how exclude certain requests based on the page names.
If this simple validation fails, you can use THIS.ErrorResponse()
to return an error message and status code, which is returned as a JSON object response with the specified status code. This method directly writes a JSON message into the Response output.
Signing in
Before you can have a valid Bearer Token to check, you of course need some mechanism to sign in. If you use pure Token authentication, rather than user security, you can use any logic to validate your user.
Here's what this might look like using an arbitrary business object:
*** A simple REST Process Method POST Signin.tp - { username: "email@test.com", password: "superSeekrit" }
FUNCTION Signin
LPARAMETER loCredentials
*** Load some business object that can authorize
loBus = CREATEOBJECT("cUser")
*** Use whatever custom Authorization you need to assign a token
IF !loBus.AuthorizeUser(loCredentials.UserName, loCredentials.Password)
RETURN THIS.ErrorResponse(loBus.cErrorMsg,"401 Unauthorized")
ENDIF
*** Create a new Session and optionally assign a mapping user id
*** that links back to a user/customer record in the Application
lcToken = THIS.oSession.NewSession(loBus.oData.UserId)
THIS.oSession.SetSessionVar("tenant",loBus.oData.TenantId)
THIS.oSession.SetSessionVar("displayname",loBus.oData.dispName)
THIS.oSession.Save() && Must explicitly save to Db
*** Return the token and expiration (or whatever you choose)
loToken = CREATEOBJECT("EMPTY")
ADDPROPERTY(loToken,"token", lcToken)
ADDPROPERTY(loToken,"expires", DATETIME() + 3600 * 24)
RETURN loToken
* Returns JSON: { token: "<token>", expires: "2023-10-23T07:00:00Z" }
ENDFUNC
The AuthorizeUser()
call could be any application logic of your choice that returns true or false.
If a user is validated we can create a new Session, and provide a mapping user id (optional). At this point you can also write additional values into the Session object that you can later retrieve if needed.
For rich client JavaScript applications you might also want to return additional information like more user information (display name, initials, sub-ids etc. as well as user settings that are applied to the user interface). You control what the return object looks like, so return whatever you need in addition to the token.
For example:
{
"token": "dre143adxq435o0",
"expires": "2023-10-23T07:00:00Z",
"user": {
"displayName": "jan doe",
"initials": "jd",
"employeeId": "4rwesi4s22om2"
},
"settings": {
"displayTimeout": 8000,
"useInjuryPrompt": false,
"useUserManager": false
}
}
REST Request Walk Through
Here's what all that looks like in a series of REST requests using West Wind WebSurge (but you can use any other tool like Postman):
Signing in to retrieve a Token
Once you have a valid token you can add it to a request in the Authorization
header (in WebSurge you can add an empty header (Authorization Bearer
) and it will auto-fill from the saved token).
Accessing a request with a valid Bearer Token
Accessing a request with invalid or missing Bearer Token
The above provides the core functionality of mapping a Bearer Token to a session record which works for simple authentication where you don't need to track a user, only if a user is signed in. If you want to also map to a wwUserSecurity
user record so you can access user information that is mapped between wwSession
and a user record, then you can add Authenticate()
.
Once we add
Authenticate()
to the above flow, the HTTP operations will look identical. The only difference is how the token, session and user are mapped on the server.
Adding User Authentication()
The basis for Authenticate()
is pretty similar to what I showed above. The difference is rather than checking for validity of a token, and creating a new token directly on the wwSession
instance, you defer that to the Authenticate()
method.
Note: In order to use
Authenticate()
,InitTokenSession()
is still required and we recommend you call it just as in the code above in OnProcessInit() as you can customize the behavior via its parameters. If you don't call it explicitly, it will be called inAuthenticate()
but always without any parameters.
The Authenticate()
method has three different modes:
Authenticate() - Validate a token from wwSession
This methods looks atwwSession
and looks for a non-empty session and if it find one uses the Authentication Session value to try and map the user id to awwUserSecurity
user record. If there is no session or the session's user entry can't be mapped a401
result is returned.Authenticate(lcUsername, lcPassword) - Authorize a user
This version of the method can be used to validate a user agains thewwUserSecurity
and it's underlying data store by validing a username and password. Username can be anything but typically will be an email address.Authenticate("LOGOUT") - Clear Session and Token Association
This operation logs out a user, removes the token to effectively sign out the user. Subsequent requests to use the existing token will then no longer succeed.
To implement then is again a 3 step process:
Initialize and Authenticate Token
This code is very similar to what I showed earlier for InitSessionToken()
but this time we use Authenticate()
to validate whether the token is valid and maps to a user:
FUNCTION OnProcessInit
...
*** IMPORTANT: InitTokenSession is required to pick up the Bearer token
*** and load or create a new session
THIS.InitTokenSession()
*** Check for pages that should bypass auth - signin always (not signout though!)
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,"testage","signin")
IF !llIgnoreLoginRequest
*** Check for Authentication here based on the token (note no parameters)
IF !this.Authenticate()
THIS.ErrorResponse("Access Denied. Please sign in first.","401 Unauthorized")
RETURN .F. && Response is handled
ENDIF
ENDIF
RETURN .T.
ENDFUNC
If a user is signed in, THIS.oSession
, THIS.oUserSecurity
and THIS.oUser
properties will all be set and ready to use.
Signing in
Next the sign in process again is very similar except this time we can use the Authenticate()
method with user name and password to parameters to directly map to a user record:
FUNCTION SignIn(loCredentials)
LOCAL loToken, llError, lcErrorMsg, lcToken, ltExpires
lcErrorMsg = ""
lcToken = ""
*** Sign in: If successful sets oUserSecurity, lIsAuthenticated, cAuthenticatedUser etc.
IF this.Authenticate(loCredentials.Username, loCredentials.Password)
lcToken = Session.cSessionId
ltExpires = Session.oData.FirstOn + Session.nSessionTimeout
*** Add any custom values you might need to store and retrieve
Session.SetSessionVar("tenant","TENANT_ID")
Session.Save() && Explicit save: Session on REST Services don't save by default
ELSE
RETURN THIS.ErrorResponse(lcErrorMsg,"401 Not Authorized")
ENDIF
*** Response
loToken = CREATEOBJECT("EMPTY")
ADDPROPERTY(loToken,"token", lcToken)
ADDPROPERTY(loToken,"expires", ltExpires)
** Add other client cachable user data here as properties if you need
RETURN loToken
ENDFUNC
This is very similar to the previous code but a little simpler as Authenticate()
abstracts away both the actual user authorization as well as creating the new session and storing the standard session variables.
Accessing Authentication Properties
Once this call completes successfully or when a Bearer token is successfully validated, all the wwprocess
authentication properties are available and you can access them in your REST service Process methods:
FUNCTION RestMethod(lvParm)
*** This should not be necessary if you called `Authenticate()`
*** and filtered out non-authenticated requests
IF !THIS.lIsAuthenticated
RETURN THIS.ErrorREsponse("Access denied: Make sure you are logged in.", "401 Unauthorized")
ENDIF
*** Capture common Auth Properties you can access
lcUsername = THIS.cAuthenticatedUser
lcDisplayName = THIS.AuthenticatedName
loUser = THIS.oUser && wwUserSecurity user instance
...
RETURN loResult
ENDFUNC
Signing out
The final piece is signing out which requires that you call a specific process method with a Bearer token (ie. authenticated) for the user to log out.
Sign out is pretty simple with authentication:
FUNCTION Signout()
THIS.Authenticate("LOGOUT")
JsonService.IsRawResponse = .T.
Response.Write( [{ message: "Successfully logged out." }])
ENDFUNC
© West Wind Technologies, 1996-2024 • Updated: 11/15/23
Comment or report problem with topic