Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
martin_voros
Active Contributor
Disclaimer: this prototype is far from production ready due to various reasons. It still shows what you can do when you have a biscuit recipe. It was also fun to do. All development was done in Netweaver version 7.31.
I really like new Mozilla Persona. It could be really nice to outsource authentication to some 3rd party in some scenarios (e.g. non-employees log on to your system). You don't have to store any passwords. If identity provider supports two-factor authentication then you get it for free.  At this early stage you have to trust Mozilla (IMO much more trustworthy than Facebook or Google) but it's a decentralized solution similar to email. So in future you can run your own corporate identity provider for internal employees. Definitely check the Mozilla pages for more information about Persona. I am going to concentrate more on SAP side.

Challenge

The Mozilla guys claim that you can add Mozilla Persona to your app in a single afternoon. So I tried to do the same for ABAP AS. I followed the quick setup from Mozilla. It has only 5 steps:
  1. Include the Persona JavaScript library on your pages.
  2. Add “login” and “logout” buttons.
  3. Watch for login and logout actions.
  4. Verify the user's credentials.
  5. Review best practices.
Note #1 – I did not bother with log out. There is a small issue with implementing log out but not a big one.
Note #2 – I did not bother with step 5 J As you will see it's not the nicest implementation. But hey, I've seen much worse code in production and this is just a prototype.
Note #3 – IE compatibility mode breaks Persona. As far as I know web dynpro for ABAP requires compatibility mode. Hence I did my testing in Firefox.
Basically, you can split changes into two groups: UI changes and backend authentication service.

Logon Screen Enhancement

The first 3 steps are related to client. So we need to include some javascript and add a button for signing in with Mozilla Persona. So I copied standard logon class CL_ICF_NW07_LOGIN and called it ZCL_PERSONA_LOGON. I modified method HTM_LOGIN to include 2 javascript libraries (Persona and jQuery). I misused variable hidden fields to add a simple HTML fragment to include javascript libraries. I used Google to host jQuery for me. jQuery is not mandatory but I liked the code with it more.
CONCATENATE
iv_hidden_fields
'<script src="https://login.persona.org/include.js"></script>'
'<script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>'
INTO new_hidden.
I also used method HTM_LOGIN to add javascript code defined in step 3. In this case I misused javascript variable. I added the following lines to it.
CONCATENATE
'var signinLink = document.getElementById(''SL2'');'
'if (signinLink) { signinLink.onclick = function() { navigator.id.request();};}'
'var currentUser =
''bob@example.com'';'
'navigator.id.watch({'
'loggedInUser: currentUser,'
'onlogin: function(assertion) {'
'$.ajax({ type: ''POST'', url: ''/persona/login'', data: {assertion: assertion},'
'success: function(res, status, xhr) { window.location.reload(); },'
'error: function(xhr, status, err) { alert("Persona Failed");navigator.id.logout(); }'
'});},'
'onlogout: function() {'
'$.ajax({type: ''POST'', url: ''/auth/logout'','
'success: function(res, status, xhr) { window.location.reload(); },'
'error: function(xhr, status, err) {}'
'});}'
'});'
iv_javascript INTO new_javascript.
The last change which I made to logon class was to add a custom link for signing in with Persona. This can be done in method ADD_CUSTOM_LINKS.
METHOD add_custom_links.                                    "#EC NEEDED
DATA link LIKE LINE OF links.

link-text = 'Sign in Mozilla Persona'.
link-href = '#'.
APPEND link TO links.
ENDMETHOD.
So I was able to do required UI changes really quickly. I changed configuration of one web dynpro app in transaction SICF to test this. I got the following logon screen.

Verifying Credentials

The second big chunk of work is implementing a backend service that validates an assertion ticket provided by client. This assertion ticket is received by client from identity provider. I implemented this service as a bespoke HTTP handler assigned to URL /persona/login (see above that this URL is called from javascript function that is registered for event onlogin).

An implementation is straight forward.

METHOD if_http_extension~handle_request.
DATA: assertion TYPE string,
persona TYPE REF TO zcl_persona,
ret TYPE i,
email TYPE string,
host TYPE string,
port TYPE string,
prot TYPE string,
audience TYPE string,
biscuit TYPE REF TO zcl_biscuit,
ticket TYPE string.

* Set HTTP code to 500
server->response->set_status( EXPORTING code = 500 reason = space ).

* Allow only POST
* XXX

* Get hostname
server->get_location( EXPORTING protocol = 'HTTPS' IMPORTING host = host port = port out_protocol = prot ).
CONCATENATE prot '://' host ':' port INTO audience.

* Get and validate assertion ticket
assertion = server->request->get_form_field_cs( 'assertion' ).

* Validate assertion ticket
persona = zcl_persona=>get_persona( 'PERSONA' ).
persona->validate_assertion( EXPORTING assertion = assertion audience  = audience
IMPORTING ret = ret email = email ).
CHECK ret EQ 0.

* Map email to user
* XXX

* Generate cookie
biscuit = zcl_biscuit=>get_biscuit( ).
ticket = biscuit->get_ticket( 'MARTIN' ).

server->response->set_cookie( name = 'MYSAPSSO2' path = '/' value = ticket ).
server->response->set_status( EXPORTING code = 200 reason = space ).

ENDMETHOD.

This service must return 200 when validation is successful. So the first think what it does is that it sets return code to 500. If anything fails then a user won't be authenticated. As I said above I outsourced everything to Mozilla. So it uses Mozilla verification service to validate assertion. I'll explain it bit more in the next section. It's really simple. You pass only 2 parameters to this service: assertion that comes from client as POST parameter and audience that must be same as client's URL. If assertion from client is correct then I map an email address returned by verification service (not implemented above, mapping should also check if user is locked) and generate a SSO cookie using class from project Biscuit. A return code is set to 200 and a cookie is sent back to user.

Assertion Validation

So the last step is to verify assertion received from client.  As I mentioned above I used Mozilla verification service. You perform a POST request with two parameters (assertion and audience) and it returns a JSON document. An implementation in ABAP looks something like this.

DATA: client TYPE REF TO if_http_client,
http_code TYPE i,
data TYPE string,
dest TYPE c LENGTH 20,
bhole TYPE string,
str TYPE string.

* Use Mozilla service to validate assertion
dest = me->rfc.
cl_http_client=>create_by_destination( EXPORTING destination = dest
IMPORTING client = client ).
client->request->set_method( if_http_request=>co_request_method_post ).

* Set assertion & audience
client->request->if_http_entity~set_form_field(
name   = 'assertion'
value  = assertion ).

client->request->if_http_entity~set_form_field(
name   = 'audience'
value  = audience ).

client->send(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state         = 2
http_processing_failed     = 3
http_invalid_timeout       = 4
OTHERS                     = 5 ).

IF sy-subrc <> 0.
* Connection failed
ret = 1.
RETURN.
ENDIF.

client->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state         = 2
http_processing_failed     = 3
OTHERS                     = 4 ).

IF sy-subrc <> 0.
* Connection failed
ret = 2.
RETURN.
ENDIF.

client->response->get_status( IMPORTING code = http_code ).
IF http_code <> 200.
ret = 3.
RETURN.
ENDIF.

data = client->response->get_cdata( ).

* Very ugly and stupid validation. Good enough for prototype
IF data(17) CS '{"status":"okay",'.
* All good, retrieve email address
* XXX
    ret = 0.
ELSE.
ret = 4.
RETURN.
ENDIF.

I also had to import Mozilla certificate into STRUST. Otherwise ABAP AS refuses connecting to Mozilla server.

How it looks?

Enough of ABAP code. Here is how it looks when you try to sign in with Persona to ABAP AS. At this moment only Yahoo implements Persona. For other email providers you need to create a Persona account with Mozilla that will be used for authentication. Mozilla will only store your email address and password. I used my Yahoo account for this demonstration.

After clicking on link Sign in Mozilla Persona you get a Persona pop up window. Because I've never used Mozilla Persona before on this computer there is no email address. It tells what you need to do but it's simple. In my case I need to enter my Yahoo email address.

So after entering my Yahoo email address I am redirected to Yahoo to authenticate.

After successful authentication with my Yahoo account I am redirected back to Persona.

Note: if I activated 2-factor authentication for this Yahoo email then I would sign in into ABAP AS using 2-factor authentication.


After this the pop up gets closed and web browser calls backend service /persona/login. It passes the assertion ticket to it. If this service returns HTTP code 200 Persona reloads the web page. At this moment my browser received MYSAPSSO2 cookie for a user mapped from email address from /persona/login. Hence reloading page displays a web dynpro application instead of logon screen. I logged on to ABAP AS without entering my SAP username and password. If I was already logged to my Yahoo email then I would not have to enter any password.

That's it. All kudos go to Mozilla guys. I am really impressed with their execution. I cut some corners (mapping, error handling) but it took me only 3 hours and 40 minutes to build this prototype. Roughly the same time as posting this blog post 🙂

3 Comments