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.
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.
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.
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.
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.
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 🙂
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
5 | |
3 | |
3 | |
2 | |
2 | |
2 | |
1 | |
1 | |
1 | |
1 |