APEX and Janrain Authentication Scheme
We had a requirement to allow authentication to Application Express (APEX) using external identity providers, like Google and Yahoo!. The solution turns out to be reasonably straight forward and uses a service provided by Janrain, called Janrain Engage (formerly RPX).
How it works
The APEX application’s login page has the Sign-in Widget from Janrain.
As shown here, the user is presented with a selection of identity providers like Google and Yahoo!, the exact list is configurable.
A registered user will select their preferred provider and login against the provider’s website handled by Janrain. Once authenticated, Janrain posts a token to a URL in the APEX application. This URL is a procedure available to APEX_PUBLIC_USER and retrieves the identity by making an API call to Janrain’s auth_info with the private RPX API Key. The user’s “verifiedEmail” is used as the identity key. This key is used to set the APEX login name “APP_USER” and to identify the user.
A page sentry is used as part of the Application’s Authentication Scheme to allow for deep linking. This allows users to be email a link to any page in the application. The initial application/page is passed to the login page as a return path.
After this, it is down to the application to limit what the user can see based on who they are logged in as.
What is needed
This is a list of the things required to make this work
- Page Sentry that redirects to Login page and allows deep linking
- Janrain account with a defined application
- Janrain token receiving procedure
- APEX Login page hosting the Sign-in Widget
- PL/JSON by jkrogsboell, lewiscunningham
- Authentication Scheme called RPX
Page Sentry that redirects to Login page and allows deep linking
A page sentry is part of the authentication scheme and is executed before a page is rendered. As you can see from the code below, the following happens:
- the APEX session id is retrieved from the cookie
- the session is validated
- If valid, the global variable g_instance is set and true is returned
- If not valid, the browser is sent a redirect where the URL is the login page
The redirect includes two variables, the original application and page. These values are used by the login page to redirect the user back once the authentication is complete.
FUNCTION PAGE_SENTRY
RETURN BOOLEAN
IS
l_username VARCHAR2(512);
l_session_id NUMBER;
BEGIN
IF USER != 'APEX_PUBLIC_USER' THEN
RETURN false;
END IF;
l_session_id := wwv_flow_custom_auth_std.get_session_id_from_cookie;
-- check application session cookie.
IF wwv_flow_custom_auth_std.is_session_valid THEN
apex_application.g_instance := l_session_id;
l_username := wwv_flow_custom_auth_std.get_username;
wwv_flow_custom_auth.define_user_session(
p_user => l_username, p_session_id => l_session_id);
RETURN true;
ELSE
OWA_UTIL.REDIRECT_URL('f?p=RPXAUTH:LOGIN:'||
nv('APP_SESSION')||
'::::RETURN_APP_ID,RETURN_PAGE_ID:'||
v('APP_ID')||
','||v('APP_PAGE_ID'));
END IF;
RETURN false;
END page_sentry;
Janrain account with a defined application
Establish an account with Janrain and select the drop down called My Applications. There is an option to “Create Application”. This opens to the following.
Use the basic application for now (shown here). Choose a name, this can be any name and will become the sub-domain of rpxnow.com. This name will appear on the screen to the users.
Janrain token receiving procedure
This procedure will be posted to by Janrain once the authentication with the chosen provider is complete. This procedure has two parameters, P and TOKEN. The TOKEN is a posted value and the P parameter is part of the token URL defined in the IFRAME Sign-in Widget.
PROCEDURE rpx_return(
p IN VARCHAR2,
token IN VARCHAR2)
AS
BEGIN
OWA_UTIL.REDIRECT_URL('f?p='||p||'::::P101_RPX_TOKEN:'||TOKEN);
END rpx_return;
APEX Login page hosting the Sign-in Widget
The login page has two regions, one for the Janrain sign-in Widget and the other is shown to the user once they have logged in. There is one button and a page process and some page item to capture details.
Janrain Region
This HTML region displays the Sign-in Widget IFRAME captured from Janrain (this is done using the Deployment->Sign-in Widget wizard). The token URL is the address of the Janrain token receiving procedure described above. I made this region conditional on the token being NULL.
The token URL needs to include the application and page IDs of the Login page and the session ID as the parameter P (look familiar?). An example is this:
http://<yourhost>:<port>/pls/apex/rpx_return?p=RPXAUTH:LOGIN:&APP_SESSION.
I have used the application/page aliases for my Login page.
After Janrain authentication, this URL is posted the TOKEN. The procedure simply redirects the browser back to the login page passing the token as a parameter. This region is now hidden.
Fetching the ID
This is the part where the identification details are captured from Janrain. Example code is available from Janrain as part of the Sign-in Wizard. I have written the Oracle PL/SQL equivalent.
FUNCTION FETCH_ID(
TOKEN IN VARCHAR2)
RETURN VARCHAR2
AS
req UTL_HTTP.REQ;
resp UTL_HTTP.RESP;
v_value VARCHAR2(1024); -- URL to post to
V_WALLET_LOCATION VARCHAR2(200) := '';
v_url VARCHAR2(200) := 'https://rpxnow.com/api/v2/auth_info';
v_param VARCHAR2(500);
v_param_length NUMBER;
RPXAPIKEY VARCHAR2(100) := 'youAPIKey from Janrain';
BEGIN
IF TOKEN IS NULL THEN
RETURN NULL;
END IF;
v_param := 'token='||token||'&apiKey='||rpxApiKey||'&format=json';
utl_http.set_wallet('file:'||v_wallet_location);
v_param_length := LENGTH(v_param);
req := UTL_HTTP.BEGIN_REQUEST (url=> v_url, method => 'POST');
UTL_HTTP.SET_HEADER(req, 'User-Agent', 'Mozilla/4.0');
UTL_HTTP.SET_HEADER (r => req,
name => 'Content-Type',
value => 'application/x-www-form-urlencoded');
UTL_HTTP.SET_HEADER (r => req,
name => 'Content-Length',
value => v_param_length);
UTL_HTTP.WRITE_TEXT (r => req,
data => v_param);
resp := UTL_HTTP.GET_RESPONSE(req);
UTL_HTTP.READ_LINE(RESP, V_VALUE, true);
UTL_HTTP.END_RESPONSE(RESP);
RETURN (v_value);
END fetch_id;
HTTPS is used to secure the fetching of the identify information, the only challenge here is getting the wallet created and holding the required ROOT of the certificate used by Janrain. I’ve used an auto login wallet, hence no password setting.
This returns the identity in JSON format. Here is an example of the returned data.
{“profile”:{“googleUserId”:”123123123132132″, “verifiedEmail”:”fred.flintstone@example.com”, “name”:{“givenName”:”Fred”, “familyName”:”Flintstone”, “formatted”:”Fred Flintstone”}, “displayName”:”fred.flintstone”, “preferredUsername”:”fred.flintstone”, “providerName”:”Google”, “identifier”:”https:\/\/www.google.com\/accounts\/o8\/id?id=AaBbCcDdEeFfGg3344FfGgGg”, “email”:”fred.flintstone@example.com”},”stat”:”ok”}
I set a page item called P101_RPX_ID to the return value of the FETCH_ID function above.
Welcome Region
This conditional HTML region displays a “Welcome” message to the end user and shows a button called “Proceed to the application”. This button invokes an “On Submit – After Computation and Validations” page process called PostLogin. It does the following:
begin
wwv_flow_custom_auth_std.post_login(
p_uname => rpx.get_username(:P101_RPX_ID),
p_session_id => nv('APP_SESSION'),
p_flow_page => v('RETURN_APP_ID')||':'||v('RETURN_PAGE_ID'),
p_preserve_case => true);
end;
This sets the APP_USER for the session and redirects the browser back to the original page. This allows for deep linking.
PL/JSON by jkrogsboell, lewiscunningham
I have used PL/JSON to decode the JSON formatted identity from RPX. It is available here on SourceForge. Note the dot notation of the path to the variable I’m using.
function get_username(
p_json_text in varchar2)
return varchar2
is
jsonObj json;
BEGIN
if p_json_text is not null
then
jsonObj := json(p_json_text);
return JSON_EXT.GET_STRING(JSONOBJ,'profile.verifiedEmail');
end if;
return null;
end get_username;
Authentication Scheme called RPX
This has a “Page Sentry Function” defined as “return page_sentry;”. As we were planning to have multiple applications sharing the same RPX authentication, I set the “Cookie Name” to RPX. This allows the session to be shared between all applications in the same workspace.
That is it
I hope some of the information held here is useful to someone else.