45K Views

OpenID Connect: Extending OAuth Based Authentication

An increasing number of platforms have recently began adopting OpenID to handle user authentication as opposed to OAuth, our knight in the shinning armour. But why?

As an enthusiast programmer, I love OAuth2. It’s an interesting concept, and very well thought. So when I first heard about OpenID, the very first thought that popped in my head was “Why?”. Like any other person, I did what most people would do when they come across something they have absolutely no idea about: ask Google. And much to my surprise, there wasn’t an awful lot explaining OpenID, not on their website anyway. I figured out that OpenID Connect (aka. OIDC) was the current OpenID Standard suited for API-like requests, but as a whole, I didn’t really understand what OpenID does and how it does it.

If you feel like you’re on the same boat as me, or just curious about the technology, this will be a good read for you as I go over the basics of OpenID Connect. For simplicity, will be using a bunch of frameworks and third-party libraries to kick start our project in PHP, but hopefully you’ll get a gist of OpenID.

What is OpenID Connect?

So what exactly is OpenID Connect? In layman’s term, it’s simply an identity layer built on top of OAuth 2. “It allows clients to verify the identity of their users through whatever authentication system they have in place, while at the time, retrieve basic information about the identity of those users”. At first glance, this doesn’t make a whole lot of sense. We’ll get back to this later and see what this statement really means, but in essence, what it simply denotes is that OpenID is not some new standard that’s different from OAuth, but that it deals with the authentication layer in a rather different approach. Under the hood, it’s the good old OAuth2 that’s at work.

OpenID consists of two main integral parties:

  • Identity Providers (IP): Identity Provider is the authenticating server that holds information about the identities of all the users you’ll be authenticating.
  • Relying Party (RP): Relying Parties is the end-user client that’ll be communicating with the authenticating server (IP) to authenticate and retrieve the information about the users.

For example, I have a website XYZ where I have implemented “Continue with Google” functionality to enable Facebook Users to login to my website through their Google Account. So to put in context of OpenID, Google here is the Identity Provider which holds information about all their Google Users, whereas XYZ website is the Relying Party which communicates with Google (IP) to authenticate their users and in return provide us with a token upon successful authentication. So in essence, what it means is that for one Identity Provider (IP), we can have multiple Relying Parties which relies on the Identity Provider to handle the authentication for users originating from that Identity Provider.

Once the authentication is successful, the Identity Provider (IP) here would in return provide us with an access token, just like any implementation with OAuth2 would. However, the difference here is that in case of OIDC, the token will be in the form of a JSON Web Token (JWT).

 

JSON Web Tokens (JWT)

JWT is an open standard for creating JSON-based access tokens which asserts a number of claims, and are designed to be compact and URL-safe. These tokens are signed using a secret key by the Identity Provider issuing the token. Since the secret key is shared between both the Identity Provider and Relying Parties, it can easily be validated by the Relying Party.

Getting to the structure of a JSON Web Token, it is simply a concatination of three different strings (with a period in between them) which holds different types of information: header, payload, and signature.

headerString.payloadString.signatureString

Structure of a JSON Web Token

Now that we know that a JSON Web Token is a combination of three different types of information, lets take a look at what they are:

  • Header: Header identifies which algorithm is used to generate the signature, as well as the type of the token.
  • Payload: Payload makes the bulk of our JSON Web Token. It contains information regarding the claims that have been made, details about the issued token as well, along with any other information we would like to pass through the token (ex: user_id, user_name, user_email, and so on).
  • Signature: Signature is used to verify the JWT Token we received from our Identity Provider. It is the combination of base64url encoding of the Header and Payload data, which is then signed by the Identity Provider by the algorithm specified in the token’s header using a secret key which is known to both the Relying Party and the Identity Provider.

So what we have is basically:

header      = encodeBase64Url('{"alg":"HS256", "typ":"JWT"}')
payload     = encodeBase64Url('{"loggedInAs":"admin","iat":1422779638}')

secret      = 'secretkey'
signature   = HMAC-SHA256(secret, header . '.' . payload)

token       = encodeBase64Url(header) . '.' . encodeBase64Url(payload) . '.' . signature

So if we utilize the signatureString part of our token, we can then easily verify the validity of our token. That being said, let’s take a look over at the payload data and understand what it contains.

JWT Payload Data:

The payload data, as in the payload part of our JSON Web Token, represents JWT Claim Set which is basically a JSON Object whose members are the claims conveyed by the JWT. So anything that comes under the Payload Data, is a JWT Claim. There are three types of JWT Claims: Registered Claims, Public Claims, and Private Claims.

 

Registered Claims:

Registered Claims are those claims which hold specific meanings to themselves. Although these are registered claims, they are not mandatory to be included in the payload data.

  • Issuer Claim (iss): Identifies the issuer of the token (Identity Provider)
  • Subject Claim (sub): Identifies the subject of the JWT (All the claims are made in context of a subject)
  • Audience Claim (aud): Identifies the recepient of the token (Relying Party)
  • Expiration Time Claim (exp): Identifies the expiration time after which the JWT is rendered unacceptable for processing
  • Not Before Claim (nbf): Identifies the time before which the JWT is not acceptable for processing
  • Issued At Claim (iat): Identifies the time at which the JWT was issued by the Identity Provider
  • JWT ID Claim (jti): Unique Identifier for the JWT

 

Public Claims:

Public Claims are those claims which we create ourselves to carry additional information along with the payload data, for example: ’email’ claim which holds the email of the authenticated user.

 

Private Claims:

The Identity Provider and Relying Part may agree to usage of certain claims that are private to them, as in claims which are neither a Registered Claim nor a Public Claim. Unlike Public Claims, Private Claims are subject to collision and should be used with caution.

 

Now that we know what a JSON Web Token comprises of, lets get to the implementation of OpenID Connect which will be utilizing JWT.

OpenID Connect

We know that a working OIDC implementation comprises of two parties: Identity Provider which will be providing user identities, and Relying Party, which will be making use of these user identities. So we’ll create two different projects, one will act as an Identity Provider, and another as Relying Party. I’ll go through the workflow of the entire authentication process, but I will only be focusing on the certain part of the project as far as code samples are concerned.

The project I’ll be building will be made using Symfony Framework for PHP. For the Identity Provider project, I’ll be using bshaffer/oauth2-server-php library to implement the OpenID Connect Server. You can go through the documentation on how to do so and implement your own OpenID Connect Server for the Identity Provider.

So let’s get to it. If you are already familiar with the workflow of an OAuth2 Authentication System, OpenID Authentication System is going to work in a very similar fashion:

  • Step 1: User on the Relying Party’s website will be redirected to Identity Provider’s Website during login for authentication.
  • Step 2: User logins on the Identity Provider’s website and upon successfull authentication is redirected back the Relying Party’s website with an authorization code as a url parameter
  • Step 3: Upon being redirected back to Relying Party’s website, the Relying Party will retrieve the authorization code from the URL and will further send a post request to retrieve the JWT access token from the Identity Provider. Once the token has been retrieved successfully, the Relying Party can then process the payload data to retrieve the authenticated user details and do whatever it needs to do

Step 1: Redirecting user to Identity Provider for Authentication

Some of the parameters here are simply there because of our choice of library we used to implement to OIDC Server on our Identity Provider. You can refer the provided documentation to read more about that.

$redirectURL = (
    'http://identityprovider/api/auth?' .
    'state=xyz' .
    '&response_type=code' .
    '&scope=openid' . 
    '&client_id=relyingparty' . 
    '&redirect_uri=' . $this->generateUrl('rp_oidc_login', [], UrlGeneratorInterface::ABSOLUTE_URL)
);

return new RedirectResponse($redirectURL);

Step 2: Handling User Authentication on Identity Provider’s OIDC Server

When the user is forwarded to the Identity Provider’s website for authentication, the identity provider will first validate the request to verify the Relying Party. Once the RP has been confirmed, the IP will retrieve the user from the user session and based on that will either present the user with a login page if the session is inactive, else will generate a JWT Access Token and map it with an Authorization Code. If the user session is active, the user will be redirected the Relying Party’s website along with the Authorization Code which will then be processed by the Relying Party.

public function handleOAuthCodeRequestAction(Request $request)
{
    $connectorService = $this->get('connector.service');
    $oauthServer = $connectorService->getOAuthServer();

    $oauthRequest = OAuth2\Request::createFromGlobals();
    $oauthResponse = new OAuth2\Response();

    // validate the authorization request
    if (!$oauthServer->validateAuthorizeRequest($oauthRequest, $oauthResponse)) {
        return new Response($oauthResponse->send());
    }

    // display an authorization form
    if (empty($_POST)) {
        exit('
            <form method="post">
                <label>Do You Authorize Relying Party?</label>
                <br />
                <input type="submit" name="authorized" value="yes">
                <input type="submit" name="authorized" value="no">
            </form>
        ');
    }

    // print the authorization code if the user has authorized your client
    $is_authorized = ($_POST['authorized'] === 'yes');
    // Once you know the logged in user, pass their identifier
    // data as the 4th parameter to map the user <-> token association
    $oauthServer->handleAuthorizeRequest($oauthRequest, $oauthResponse, $is_authorized, json_encode([
        'id' => 'user_id',
        'email' => 'user_email'
    ]));

    if ($is_authorized) {
        header("Location: " . $oauthResponse->getHttpHeader('Location'));
    } else {
        exit('User permission denied');
    }

    return new Response($oauthResponse->send());
}

Step 3: Process the Authorization Code and Retrieve JWT Token

From the Relying Party’s Perspective:

if ($request->query->get('code') != null && $request->query->get('state') != null) {
    $authorizationCode = $request->query->get('code');
    $requestURL = (
        'http://identityprovider/api/token?' .
        'grant_type=authorization_code' .
        '&code=' . $authorizationCode .
        '&redirect_uri=' . $this->generateUrl('rp_oidc_login', [], UrlGeneratorInterface::ABSOLUTE_URL)
    );
    $curlHandler = curl_init();
    curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curlHandler, CURLOPT_URL, $requestURL);
    $curlResponse = curl_exec($curlHandler);
    curl_close($curlHandler);

    $tokenResponse = json_decode($curlResponse, true);
    if (!empty($tokenResponse['id_token'])) {
        $jwtBody = explode('.', $tokenResponse['id_token']);
        $jwtPayload = json_decode($this->base64url_decode($jwtBody[1]), true);
        $userDetails = json_decode($jwtPayload['sub'], true);

        // We now have our user's details which we can process in whichever way we want
    } else {
        exit('login failed');
    }
}

From the Identity Provider’s Perspective:

public function handleOAuthTokenRequestAction(Request $request)
{
    $connectorService = $this->get('connector.service');
    $oauthServer = $connectorService->getOAuthServer();

    $codeResponse = json_encode($oauthServer->getStorage('authorization_code')->getAuthorizationCode($request->request->get('code')));
    return new Response($codeResponse);
}

 

Conclusion

So we just went over the basics of OpenID Connect. It’s been employed by many different platforms, most notably Google. The generated JSON Web Token can be further used to perform api queries depending on the functionalities of the respective platform. In contrast, it’s not very different from OAuth, but at the same time, it’s very different. It’s different in the sense that it extends the functionalities of OAuth2, and with that in place, the generated JSON Web Token can carry additional information and is a bit more secure now that we can cross-verify our token to make sure that it indeed originated from the expected endpoint. In my opinion, it makes sense when you have a bunch of relying parties and would like to use the same token in place for authentication of users across these different platforms which are your relying parties. For example, Google has a lot of platforms, but the users they have are common to one Identity Provider, which is Google itself.

But at the same time, if there’s only a single Relying Party, then I would rather prefer working with OAuth2.

Category(s) API Security
. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

css.php