Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
ChrisPaine
Active Contributor

I'm just back from my Christmas/New Year's  break, where I promised my family that I wouldn't touch an SAP system the entire time, I might just about have succeed, at least I didn't use SAP GUI or Eclipse the whole time, though I'm sure at least some of online sites that I used probably had SAP back-ends. Whilst my body is still trying to make sense of the amount of food that I ate over the holiday period, I thought I'd better get these blogs I've been promising out. (Picture above was of the awesome Christmas lunch that we had with some amazing baked ham thanks to my brother-in-law Stew).

So first in the series! How to use OAuth to validate a user in a Netweaver Cloud application.

Why?

Well, NetWeaver Cloud has some pretty cool built-in authentication abilities, especially if you are using SAML based authentication. Well, actually, only if you are using SAML based authentication :sad: . This is great for many enterprise use cases where you have a central identity provider which will respond with SAML responses to identity challenges, but not so useful in the case that you don't. Enterprise loves SAML (well actually I've heard of some that don't due to the latency it introduces (if you've logged into SCN recently then you'll be aware of that!) but in general terms, as a consumer we don't use a lot of SAML yet. The use case that we're exploring does not have the user authenticating using any centrally known identity management server, but instead takes the more consumer based approach and uses Twitter, Facebook and Google as the user authentication tools.

What  is an authenticated user anyway?

In my application, I need to track that the currently "logged in" user is the who they say that they are. How they prove to my application that they are who they say that they are is, to me, neither here nor there, as long as it isn't too much work for me to implement! There are many different ways of doing this. The standard one being the old username/password combo. That has a lot of drawbacks - not least in securing it. So to me an authenticated user is one that I trust is who it says it is, next step figure out how to offload the hard authentication/user management work to someone else!  :smile:

Welcome OAuth

OAuth 1.0a and 2.0 are "standardised" protocols for sharing authentication and privilege information between applications. According to the OAuth website OAuth is:

"An open protocol to allow secure authorization in a simple and standard method from web, mobile and desktop applications."

Now I say "standardised" in quotes, because it seems that no two OAuth providers implement the protocol in exactly the same way - there are minor differences between them all, however, these aren't too hard to code around. It's probably worth having a read of Eran Hammer's OAuth 2.0 and the Road to Hell post to understand a little bit about the differences between OAuth1.0a and 2.0 and why good ole enterprise companies (who love SAML) managed to get us in that hole! Eran has some great resources on OAuth 1.0a which I used last year to code an OAuth1.0a ABAP to Google application (which I still have yet to blog about - must put on todo list!), check out his web site for further details.

How does it all work?

If you can handle my ummms and errrs - the following video is a relatively simple explanation of how the solution works:

In a nutshell:

  1. User accesses application (no/invalid access cookie)
  2. Application redirects user to login page
  3. User chooses service to use to authenticate
  4. User authenticates on remote service
  5. Remote service redirects user back to application - includes token to allow access
  6. Application validates token/exchanges it for access token
  7. Application generates cookie for user access
  8. User now logged in.

But really- how does it work?

I'll take you through a user's journey in my NetWeaver Cloud app, and try to explain at a quite detailed technical level what is happening here.

Access and redirect

All of the pages in my application run a little ECMAScript on load (know to everyone else apart from picky sods like myself as JavaScript). Note a reasonable amount of jQuery is used.

function authenticate() {

       var authCookie = $.cookie("DiscoveryTimeSheet");

       if (authCookie == null) {

              goToLogin();

       } else {

              // check that cookie is actually valid

              var jsonData = $.parseJSON($.ajax({

                     url : "AuthenticatedUser",

                     dataType : "json",

                     async : false

              }).responseText);

              if (jsonData.authenticated != "true") {

                     goToLogin();

              } else {

                     // allow user to logout

                     $("#logout").click(logOut);

                     // populate the various fields on the screen that use user data

                     $("#username").html(jsonData.userdetails.username);

                     $("#userid").html(jsonData.userdetails.userid);

                     $("#userimage").attr("src", jsonData.userdetails.imageURL);

                     switch (jsonData.userdetails.signedInWith)

                     {

                     case "TWITTER": $("#socialIcon").attr("class","zocial icon twitter");

                     break;

                     case "FACEBOOK": $("#socialIcon").attr("class","zocial icon facebook");

                     break;

                     case "GOOGLE": $("#socialIcon").attr("class","zocial icon google");

                     break;

                     }

              }

       }

}

function goToLogin() {

       window.location.replace("login.html");

}

function logOut(authCookie){

       $.removeCookie("DiscoveryTimeSheet", { path: '/DiscoveryTimesheetDemo'});

        window.location.href = "login.html";

}

This script when run - called in the onload of the body of each and every page of my app - eg:

<!DOCTYPE html>

<html>

<head>

<meta charset="ISO-8859-1">

<title>Login to page</title>

<link href="css/zocial/zocial.css" rel="stylesheet" type="text/css" />

<link href="css/discovery.css" rel="stylesheet" type="text/css" />

<link href="css/timesheet.css" rel="stylesheet" type="text/css" />

<script type="text/javascript" src='script/jquery-1.8.2.min.js'></script>

<script type="text/javascript" src='script/jquery.cookie.js'></script>

<script type="text/javascript" src='script/jquery.cycle.js'></script>

<script type="text/javascript" src='script/discovery.js'></script>

<script type="text/javascript" src='script/authenticate.js'></script>

<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">

</head>

<body onload="authenticate()">

...

Even if the user decides to bypass the script (quite possible) they would still not get any data or be able to interact on the other web pages. All calls to retrieve data (all data is brought into the web page and  updated via AJAX calls) pass the session cookie as a validating token. As per Kick's like a Mule - Bouncer if your name's not down, you're not coming in, no valid cookie, no content.

Inside the application each AJAX servlet first checks the cookie to see if the user is authorise to read/update data.

For example in this servlet which lists the mobile devices that have been paired with the user's account:

       /**

        * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

        * response)

        */

       protected void doGet(HttpServletRequest request,

                     HttpServletResponse response) throws ServletException, IOException {

              try {

                     TimeSheetMobileApp device = authoriseAndValidate(request);

                     outputResponse(device, request, response);

              } catch (UnauthorisedAccessException e) {

                     logger.info("Unauthorised Access attempt - GET");

                     response.setStatus(403);

              } catch (DeviceNotFoundException e) {

                     logger.debug("Device not found - GET");

                     response.setStatus(404);

              }

       }

and some pretty ugly code which is used to check if a user is valid:

       public static TimeSheetUser getUserIfValid(HttpServletRequest request,

                     UserDataStore userData) {

              // check if user is authenticated to the app - do they have a valid

              // cookie?

              Cookie[] requestCookies = request.getCookies();

              TimeSheetUser user = null;

              if (requestCookies != null) {

                     for (int i = 0; requestCookies.length > i && user == null; i++) {

                           Cookie cookie = requestCookies[i];

                           if (cookie.getName().equals(AuthenticateUser.COOKIE_NAME)) {

                                  // check if this is a valid cookie, and not just a made up

                                  // one

                                  user = userData.getUserFromCookie(cookie.getValue());

                                  logger.debug("Cookie value found: " + cookie.getValue());

                           }

                     }

              }

              return user;

       }

       public static final String COOKIE_NAME = "DiscoveryTimeSheet";

}

So this is what the user sees if they enter the URL for the app (any page) they are redirected to the login page.

Signing and requesting permission from an OAuth provider

Once the user is on the login page they then have to click on one of the login buttons - e.g. Twitter:

This then sends them to the Twitter website, but first it makes sure that the call to the site is signed.

This is done using (unsurprisingly) with some ECMAScript and a servlet:

$(function() {

       // associate logon functions with the login buttons

       $("#twitterLogin").click(loginTwitter);

       $("#googleLogin").click(loginGoogle);

       $("#facebookLogin").click(loginFacebook);

});

function loginTwitter() {

       var jsonData = $.parseJSON($.ajax({

              url : "TwitterLogin",

              dataType : "json",

              async : false

       }).responseText);

       if (jsonData.AuthURL != null) {

              window.location.href = jsonData.AuthURL;

       }

}

/**

        * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

        * response)

        */

       protected void doGet(HttpServletRequest request,

                     HttpServletResponse response) throws ServletException, IOException {

              String myURL = request.getRequestURL().toString();

              String twitterCallbackURL = myURL.substring(0,myURL.lastIndexOf("/")) + "/TwitterCallback";

              OAuthService service = new ServiceBuilder().provider(MyTwitterApi.class)

                           .apiKey(OAuthKeys.getTwitterConsumerKey())

                           .apiSecret(OAuthKeys.getTwitterConsumerSecret())

                           .callback(twitterCallbackURL).build();

              Token requestToken = service.getRequestToken();

              String authURL = service.getAuthorizationUrl(requestToken);

              JsonObject auths = new JsonObject();

              auths.addProperty("AuthURL", authURL);

              response.getWriter().println(auths.toString());

              response.setContentType("application/json");

       }

The magic here is done by the OAuthService class for which I have happily used Scribe which is a really simple lib for OAuth in Java. I did have to make a couple of changes (note I am using "MyTwitterApi" as a provider not the generic Scribe one (this is because I wanted to use the "Authenticate" API and not the "Authorise" one.)) Likewise for Google access, I had to find one of the yet merged forks which adds OAuth 2.0 functionality for Google. However, the lib is very simple to use (and having coded from scratch an OAuth 1.0 implementation in ABAP for Google I can attest that this is a hell of a lot simpler).

The user is then directed to the Twitter website:

The user is prompted to sign in - or not if like me they are already signed in... They are then given some pretty clear info about what they are about to authorise. N.B. I can't read your DMs (or more importantly perhaps, send them!)

Once they either cancel, or allow the use of their Twitter account, the Twitter website will redirect the user back to the redirection URL that I specified as part of the set up.

The servlet that serves this redirection then checks that the user has authenticated:

/**

* Servlet implementation class TwitterCallBackServlet

*/

public class TwitterCallBackServlet extends HttpServlet {

       private static final long serialVersionUID = 1L;

       Logger logger = LoggerFactory.getLogger(TwitterCallBackServlet.class);

       /**

        * @see HttpServlet#HttpServlet()

        */

       public TwitterCallBackServlet() {

              super();

       }

       /**

        * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

        * response)

        */

       protected void doGet(HttpServletRequest request,

                     HttpServletResponse response) throws ServletException, IOException {

              // get the token and the verifier

              String tokenString = request.getParameterValues("oauth_token")[0];

              String verifierString = request.getParameterValues("oauth_verifier")[0];

              if (tokenString == null || verifierString == null) {

                     // didn't authenticate... boo hoo

                     // redirect back to login page

                     redirectWithFail(request, response);

              } else {

                     // we did authenticate, now need to check if data returned is real

                     // or faked - i.e. we have to call known twitter api

                     // and attempt to get details

                     // since this is happening from server side rather than client side,

                     // much harder for anyone to cause us an issue

                     String myURL = request.getRequestURL().toString();

                     String twitterCallbackURL = myURL.substring(0,

                                  myURL.lastIndexOf("/"))

                                  + "/TwitterCallback";

                     OAuthService service = new ServiceBuilder()

                                  .provider(MyTwitterApi.class)

                                  .apiKey(OAuthKeys.getTwitterConsumerKey())

                                  .apiSecret(OAuthKeys.getTwitterConsumerSecret())

                                  .callback(twitterCallbackURL).build();

                     Token requestToken = new Token(tokenString,

                                  OAuthKeys.getTwitterConsumerSecret());

                     Verifier verifier = new Verifier(verifierString);

                     try {

                           Token accessToken = service.getAccessToken(requestToken,

                                         verifier);

                           if (accessToken == null) {

                                  // someone is trying to pull a fast one here...

                                  redirectWithFail(request, response);

                           } else {

                                  // have valid response from Twitter - create a cookie for

                                  // the user and update their login details in DB

                                  // first of all, need to find out who this person actually

                                  // is!

                                  OAuthRequest detailsRequest = new OAuthRequest(Verb.GET,

                                                "https://api.twitter.com/1.1/account/verify_credentials.json");

                                  detailsRequest.addQuerystringParameter("skip_status",

                                                "true");

                                  detailsRequest.addQuerystringParameter("include_entities",

                                                "false");

                                  service.signRequest(accessToken, detailsRequest);

                                  Response detailsResponse = detailsRequest.send();

                                  String guid = UUID.randomUUID().toString();

                                  // now create/update a user record for this person

                                  TimeSheetUser user = new TimeSheetUser();

                                  user.setAccessCookie(guid);

                                  user.setAccessToken(accessToken.getToken());

                                  user.setUserType(TimeSheetUserType.TWITTER);

                                  JsonObject twitterDetails = (new JsonParser()).parse(

                                                detailsResponse.getBody()).getAsJsonObject();

                                  String userId = twitterDetails.get("screen_name")

                                                .getAsString();

                                  user.setUserId(userId);

                                  String userName = twitterDetails.get("name").getAsString();

                                  user.setName(userName);

                                  String imageURL = twitterDetails

                                                .get("profile_image_url_https").getAsString()

                                                .replace("\\", "");

                                  user.setImageURL(imageURL);

                                  logger.debug(detailsResponse.getBody() + "\n");

                                  logger.debug("\n" + "UserId: " + userId + " Cookie: "

                                                + guid + " Access Token: " + accessToken.getToken()

                                                + " Display Name: " + userName + " Image URL: "

                                                + imageURL);

                                  // now store all this info into the DB

                                  UserDataStore myDataStore = new UserDataStore();

                                  myDataStore.store(user);

                                  String loginURL = myURL

                                                .substring(0, myURL.lastIndexOf("/"))

                                                + "/index.html";

                                  response.setStatus(302);

                                  response.setHeader("Location", loginURL);

                                  Cookie cookie = new Cookie(AuthenticateUser.COOKIE_NAME,

                                                guid);

                                  response.addCookie(cookie);

                           }

                     } catch (OAuthConnectionException e) {

                           redirectWithFail(request, response);

                     }

              }

       }

       private void redirectWithFail(HttpServletRequest request,

                     HttpServletResponse response) {

              String myURL = request.getRequestURL().toString();

              String loginURL = myURL.substring(0, myURL.lastIndexOf("/"))

                           + "/login.html?FailedAuthentication=Twitter";

              response.setStatus(302);

              response.setHeader("Location", loginURL);

       }

}

If the user is successfully authenticated (not just a token sent to the servlet, but I can actually verify that access token with the OAuth provider) then I store/update their details in my application's database. I also request a bit of information about the user (like what their name and public profile picture URL is). The application then causes a 302 redirect to the index page of the application (or to the login page if the login failed for any reason).

Setting up the OAuth API access, or why I still have a Facebook account.

In order for my application and the OAuth provider to be able to communicate and trust each other, there needs to be some way to ensure that communications between the two are signed. This is where the secret key concept comes in. Both the provider and I have a shared secret key which we don't tell anyone else about. I sign my requests with this key so that the provider knows that what it is sending me actually comes from me.

In order to do this, I need to set up the OAuth access on the OAuth provider's system. Below are some screen shots of how this was done with Twitter.

The set up for Twitter is quite simple - just go to the dev.twitter.com/apps URL and add your app.

Here the most important bits of the set up are shown - the application/consumer key which I use to tell Twitter that it is authenticating for my particular app and the secret (blurred out) which I use to tell Twitter that is really is my app that is doing the calling of the API.

You can set a bunch of different details - including the default callback URL.

Some OAuth providers set ups will allow you to list multiple callback addresses, some insist that you specify as part of the request call, it really depends on the OAuth provider. However, all providers will require you to register (and in the case of my examples - including Facebook you need to have an account in order to register an OAuth application). Some OAuth providers have some pretty arduous requirements to meet to ensure that you are who you say you are and have authority to register your domain for OAuth access (I certainly have had fun with Google with their OAuth1.0a setup on this one!). However, generally it is pretty simple to set up.

And that's a wrap

So with the user successfully authenticated I can now go about running the rest my application!

Joanna Chan and I will be posting a few more blogs about this mobile/cloud based application that we are building, so please stay tuned, I'll post a link to the next blog in the series once Jo or I write it.

The usual disclaimer applies: all the stuff I write about I do with the general thought that most people aren't going to read it, so thank you for getting this far. Hopefully all the code, examples and explanations I've put in here are error free, but I can't vouch for that! Although if you find any mistakes please comment below and I will attempt to fix! Any opinions, postulations and mistakes are my own, unless they are really good,  in which case I allow my wonderful employer who's logo I've splashed around on these pages to take some credit too.

17 Comments
Labels in this area