Add Basic Authentication against Access Manager to an Application

This topic has 6 replies, 2 voices, and was last updated 4 years, 10 months ago by chris-fry.

  • Author
    Posts
  • #19400
     chris-fry
    Participant

    I have a web application with no built in security isolated in a secured network and would like to expose this to a wider network via IG, using OpenAM for AuthN/AuthZ.

    My consumer can only support Basic Authentication at this stage.

    Two questions:
    – Can I configure a Basic Authentication filter that uses OpenAM (or even DJ/LDAPS) as the authentication provider? If so, is there an example of this somewhere I can refer to?
    – Can I use OpenAM authorisation policies following Basic Authentication?

    I can use a scripted filter if required, but would prefer to use out of the box filters if possible.

    Here’s some rough psuedocode:

    If request does not have a Basic Authentication header
      Return a 401 error
    Else if the request does have a Basic Authentication header
      Verify the basic authentication header against OpenAM
      If the basic authentication header is verified
        Confirm the user is authorised to perform the requested operation using OpenAM
        If the user is authorised to perform the requested operation
          Pass the request through to the web application
          Return the response to the user
        Else (user is not authorised)
          Return a 403 error
      Else (basic authentication header is not verified)
        Return a 401 error
    
    #19403
     anonymouq
    Participant

    This reply has been reported for inappropriate content.

    TEST

    #19432
     Joachim Andres
    Participant

    Hi Chris,

    Thanks for your use case. In your case, I suppose that your client could not log with AM in the first place using Basic Authentication and then present the SSO token to IG ? The latter would be the typical flow, i.e. authentication at AM, enforcement at IG.

    If correct, I see why in your case the HttpBasicAuthFilter (https://backstage.forgerock.com/docs/ig/5.5/reference/#HttpBasicAuthFilter) would not do the job. We are looking at a atypical flow here. But I guess you don’t have the choice.

    So I think a ScriptableFilter or HeaderFilter could pick up the Authorization Header, extract and convert attributes and values and another script would execute the authentication against DS or AM (for inspiration: https://backstage.forgerock.com/docs/ig/5.5/gateway-guide/#scripting-ldap-auth).

    Cheers,
    Joachim

    #19433
     Joachim Andres
    Participant

    Hi Chris,

    Missed your 2nd question on if you can use AM policies: This depends on how you pass on the subject. A possibility is that the filters describe above set the sso cookie on behalf of AM (an atypical flow though). In this case, the out-of-the-box PolicyEnforcementFilter can be used further in the chain to enforce AM policies.

    Cheers,
    Joachim

    #19434
     chris-fry
    Participant

    Thanks for your response, Joachim.

    It’s getting close – I’ve used an approach similar to what you’ve described, decoding the Basic Auth header and using an LDAP provider for authentication.

    I can’t figure out how to call OpenAM from the scriptable filter however. I’ve done this from a number of other clients (Python, Postman etc.), but can’t figure out how to do it with IG/Groovy scripts :/

    If I can get that working and save the SSO Token into the context, then I can just chain this to the authorization PEP filter and all will be well :)

    Any advice on this?

    – Chris

    #19436
     Joachim Andres
    Participant

    Hi Chris,

    For AM authentication, you could use the AM REST endpoints, see https://backstage.forgerock.com/docs/am/5.5/dev-guide/index.html#sec-rest-authentication
    For a “simple” authentication process, this should be straightforward. For a more complex one, you’d have to deal with callbacks etc.

    You might want to read this for using REST calls from within IG to an external service: https://backstage.forgerock.com/knowledge/kb/article/a77687377

    Cheers,
    Joachim

    #19588
     chris-fry
    Participant

    Ok – got it working. I created a scriptable filter that checks for, decodes and validates a basic authentication header against Access Manager, then passes the resulting token to the next filter/handler in the chain. Then I just added this in front of an SSO, Policy Enforcement Point filter chain (as per this guide). I wanted to avoid non-standard libraries, so went for the URL class rather than using http-builder. Took some trial and error, but got there in the end.

    Example Config.:

    {
      "name": "app1",
      "monitor": false,
      "baseURI": "http://app1.lab.local:9090",
      "condition": "${request.uri.host eq 'app1.example.com'}",
      "handler": {
        "type": "Chain",
        "config": {
          "filters": [
            {
              "type": "ScriptableFilter",
              "config": {
                "type": "application/x-groovy",
                "file": "BasicAuthToSsoFilter.groovy",
                "args": {
                  "openamUrl": "http://openam.lab.local:8080/openam",
                  "realm": "/",
                  "cookieName": "iPlanetDirectoryPro"
                }
              }
            },
            {
              "type": "SingleSignOnFilter",
              "config": {
                "openamUrl": "http://openam.lab.local:8080/openam"
              }
            },
            {
              "type": "PolicyEnforcementFilter",
              "config": {
                "openamUrl": "http://openam.lab.local:8080/openam",
                "pepUsername": "ig",
                "pepPassword": "examplepassword123",
                "application": "app1",
                "ssoTokenSubject": "${contexts.ssoToken.value}"
              }
            }
          ],
          "handler": "ClientHandler"
        }
      }
    }

    And the script, placed in $OPENIG_BASE/scripts/groovy/BasicAuthToSsoFilter.groovy:

    def basicAuthPrefix = "Authorization: Basic "
    def basicAuthHeaderFormat = "(?i)\\[${basicAuthPrefix}[a-zA-Z\\d]+[=]*\\]"
    
    def amAuthnPath = ""
    if(realm == "/") {
      amAuthnPath = "/json/authenticate"
    }
    else {
      amAuthnPath = "/json/${realm}/authenticate"
    }
    
    // If request does not have a Basic Authentication header,
    //  pass on to the next filter
    if (request.headers.Authorization == null) {
      return next.handle(context, request)
    }
    // Else, if there is an authentication header
    else {
      try {
    
        def authzHeader = request.headers.Authorization.toString()
    
        // Validate header format
        assert authzHeader ==~ /${basicAuthHeaderFormat}/
    
        // Decode the header into username and password
        // Remove square brackets enclosing header
        authzHeader = authzHeader.substring(1, authzHeader.length() - 1)
        // Remove leading header string
        authzHeader = authzHeader.substring(basicAuthPrefix.length())
    
        def userPass = new String(authzHeader.decodeBase64(), "UTF-8").split(":")
        def username = userPass[0]
        def password = userPass[1]
    
        // Verify the basic authentication header against OpenAM
    
        def url = new URL("${openamUrl}${amAuthnPath}")
        def connection = url.openConnection()
        connection.setRequestMethod("POST")
        connection.setRequestProperty('X-OpenAM-Username', username)
        connection.setRequestProperty('X-OpenAM-Password', password)
        connection.setRequestProperty('Content-Type', 'application/json')
        connection.connect()
        def authnResponse = connection.content.text
        def slurper = new groovy.json.JsonSlurper()
        def tokenId = slurper.parseText(authnResponse).tokenId
    
        // Remove authentication header
        request.headers.remove('Authorization')
    
        // Add the SSO Token to the request
        request.getHeaders().put("Cookie", cookieName + '=' + tokenId)
    
        // Pass to the next handler
        return next.handle(context, request)
      }
      catch (ArrayIndexOutOfBoundsException e) {
        // Bad header, pass through
        return next.handle(context, request)
      }
      catch (AssertionError e) {
        // Bad header, pass through
        return next.handle(context, request)
      }
      catch (UnknownHostException e){
        // Bad OpenAM URL, or OpenAM down. Pass through
        return next.handle(context, request)
      }
      catch (Exception e){
        // Unknown exception, pass through
        return next.handle(context, request)
      }
      finally {
      }
    }

    Thanks again for your assistance, Joachim.

Viewing 7 posts - 1 through 7 (of 7 total)

You must be logged in to reply to this topic.

©2022 ForgeRock - we provide an identity and access platform to secure every online relationship for the enterprise market, educational sector and even entire countries. Click to view our privacy policy and terms of use.

Log in with your credentials

Forgot your details?