Digest based Single-Sign-On

Problem this snippet solves:

Sometimes, you are asked to implement some unusual Single Sign On methods. This code helps to deal with Digest based SSO when configured. This specific implementation support a Quality-of-protection set to "auth".

How to use this snippet:

Proof of Concept

Generate a valid Digest response

when hitting "/test", the irule build the Digest response and log it to the ltm log file.

when RULE_INIT {
    set static::nonce "MDgvMjUvMjAxNyAwOToyNTo0Nw"
    set static::user "testuser"
    set static::password "testpass"
    set static::realm "testrealm"
    set static::method "GET"
    set static::uri "/testuri"
    set static::client_nonce "389db6597243daf2"
    set static::nonce_count "00000001"
}

when HTTP_REQUEST {

    # working test

    if { [HTTP::uri] eq "/test" } {
        binary scan [md5 "$static::user:$static::realm:$static::password"] H* ha1
        log local0. "HA1 = $ha1"

        binary scan [md5 "$static::method:$static::uri"] H* ha2

        log local0. "HA2 = $ha2"

        binary scan [md5 "$ha1:$static::nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response
        log local0. "response = $response"

    }
}

Play Digest SSO when receiving a 401 response from the backend

note : Client Nonce is currently a static variable. Must be generated within the irule instead.

when RULE_INIT {
  set static::user "testuser"
  set static::password "testpass"
  set static::client_nonce "389db6597243daf2"
  set static::nonce_count "00000001"
}

when HTTP_REQUEST {

  # set vars required for Digest SSO

  set uri [HTTP::uri]
  set method [HTTP::method]
  set retried 0

  # insert a dummy text. Help to inject Digest SSO

  HTTP::header replace Authorization "irule-test-digest-sso"
  set request [HTTP::request]
  HTTP::header remove Authorization

}

when HTTP_RESPONSE {
  if { [HTTP::status] contains "401" and [HTTP::header exists "WWW-Authenticate"] and [HTTP::header "WWW-Authenticate"] contains "Digest" and $retried == 0 } {

      set www_auth [HTTP::header "WWW-Authenticate"]

      set fields [split $www_auth ","]

      set realm [lindex [split [lindex $fields 0] "="] 1]
      set nonce [lindex [split [lindex $fields 1] "="] 1]

      # retrieve username and password from wherever you want. Can be APM, Basic authentication, ...
      binary scan [md5 "$static::user:$realm:$static::password"] H* ha1

      binary scan [md5 "$method:$uri"] H* ha2

      binary scan [md5 "$ha1:$nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response

      set retried 1

      set auth_value "Digest username=\"$static::user\", realm=\"$realm\", nonce=\"$nonce\", uri=\"$uri\", algorithm=MD5, response=\"$response\", opaque=\"0000000000000000\", qop=auth, nc=$static::nonce_count, cnonce=\"$static::client_nonce\""

      # insert Authorization header with Digest
      set updated_request [string map "$find $auth_value" $request] 

    # resend the request with the Authorization header filled
      HTTP::retry $updated_request

  } else {
      set retried 0
  }
}

Code :

when RULE_INIT {
  set static::user "testuser"
  set static::password "testpass"
  set static::client_nonce "389db6597243daf2"
  set static::nonce_count "00000001"
}

when HTTP_REQUEST {

  # set vars required for Digest SSO

  set uri [HTTP::uri]
  set method [HTTP::method]
  set retried 0

  # insert a dummy text. Help to inject Digest SSO

  HTTP::header replace Authorization "irule-test-digest-sso"
  set request [HTTP::request]
  HTTP::header remove Authorization

}

when HTTP_RESPONSE {
  if { [HTTP::status] contains "401" and [HTTP::header exists "WWW-Authenticate"] and [HTTP::header "WWW-Authenticate"] contains "Digest" and $retried == 0 } {

      set www_auth [HTTP::header "WWW-Authenticate"]

      set fields [split $www_auth ","]

      set realm [lindex [split [lindex $fields 0] "="] 1]
      set nonce [lindex [split [lindex $fields 1] "="] 1]

      # retrieve username and password from wherever you want. Can be APM, Basic authentication, ...
      binary scan [md5 "$static::user:$realm:$static::password"] H* ha1

      binary scan [md5 "$method:$uri"] H* ha2

      binary scan [md5 "$ha1:$nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response

      set retried 1

      set auth_value "Digest username=\"$static::user\", realm=\"$realm\", nonce=\"$nonce\", uri=\"$uri\", algorithm=MD5, response=\"$response\", opaque=\"0000000000000000\", qop=auth, nc=$static::nonce_count, cnonce=\"$static::client_nonce\""

      # insert Authorization header with Digest
      set updated_request [string map "$find $auth_value" $request] 

    # resend the request with the Authorization header filled
      HTTP::retry $updated_request

  } else {
      set retried 0
  }
}

Tested this on version:

11.6
Updated Jun 06, 2023
Version 2.0
  • 1/ The server nonce value might contain equal sign so the above code will chop it off in an unintended way.

     

    2/ It would need a condition block in HTTP_REQUEST to identify if it is a request from HTTP::retry.

     

    when RULE_INIT {
        set static::user "testuser"
        set static::password "testpass"
        set static::client_nonce "389db6597243daf2"
        set static::nonce_count "00000001"
    }
    when CLIENT_ACCEPTED {
        set retried 0
    }
    when HTTP_REQUEST {
        if { ${retried} == 0 } {
             set vars required for Digest SSO
            set uri [HTTP::uri]
            set method [HTTP::method]
            set retried 0
    
             insert a dummy text. Help to inject Digest SSO
            HTTP::header replace Authorization "irule-test-digest-sso"
            set request [HTTP::request]
            HTTP::header remove Authorization
        }
    }
    when HTTP_RESPONSE {
        if { [HTTP::status] contains "401" and [HTTP::header exists "WWW-Authenticate"] and [HTTP::header "WWW-Authenticate"] contains "Digest" and $retried == 0 } {
            set www_auth [HTTP::header "WWW-Authenticate"]
             basically chop the "realm=" and "nonce=" using strictly string functions
            set realm [string range ${www_auth} [expr {[string first "realm=" ${www_auth}] + 7}] [expr {[string first "," ${www_auth} [string first "realm=" ${www_auth}]] - 2}]]
            set nonce [string range ${www_auth} [expr {[string first "nonce=" ${www_auth}] + 7}] [expr {[string first "," ${www_auth} [string first "nonce=" ${www_auth}]] - 2}]]
    
             retrieve username and password from wherever you want. Can be APM, Basic authentication, ...
            binary scan [md5 "$static::user:$realm:$static::password"] H* ha1
            binary scan [md5 "$method:$uri"] H* ha2
            binary scan [md5 "$ha1:$nonce:$static::nonce_count:$static::client_nonce:auth:$ha2"] H* response
    
            set retried 1
            set auth_value "Digest username=\"$static::user\", realm=\"$realm\", nonce=\"$nonce\", uri=\"$uri\", algorithm=MD5, response=\"$response\", qop=auth, nc=$static::nonce_count, cnonce=\"$static::client_nonce\""
    
             insert Authorization header with Digest
            set updated_request [string map [list "irule-test-digest-sso" "$auth_value"] $request] 
    
             resend the request with the Authorization header filled
            HTTP::retry $updated_request
        } else {
            set retried 0
        }
    }
    

    Hopefully this would help someone working on this trick.