OAuth 2.0 Dynamic Client Registration

Problem this snippet solves:

OAuth 2.0 is now supported in version 13. Client applications need to be defined manually in the Web UI. We developed an irule allowing a client application to self register.

How to use this snippet:

This code makes calls to external functions : json2dict and HTTP Super SIDEBAND Requestor. You need to add them to your set of irules on the BIG-IP before configuring the client app registration service.

json2dict

proc json2dict JSONtext {
    string range [
        string trim [
            string trimleft [
                string map {\t {} \n {} \r {} , { } : { } \[ \{ \] \}} $JSONtext
                ] {\uFEFF}
            ]
        ] 1 end-1
}

Workflow

The client application will do the following request :

POST /f5-oauth2/v1/client-register HTTP/1.1
Host: oauthas.example.com
User-Agent: curl/7.47.1
Accept: */*
Content-Length: 29
Content-Type: application/x-www-form-urlencoded

username=user&password=pwd

This request can be achieved using cURL :

curl -k -vvv https://oauthas.example.com/f5-oauth2/v1/client-register -d 'username=user&password=pwd'

Quick notes

The irule is configured to create Client Applications with Resource Owner Password Credentials Grant (ROPC) mode activated. The irule needs to be modified to activate other modes

Update (2018-05-01)

  • Add a way to configure static client_id and client_secret

Update (2018-01-12)

  • URI decode username and password parameters

Update (2017-11-07)

  • Use HTTP::collect to workaround issues with large body

Update (2017-10-25)

  • Enhance JSON result parsing and attribute retrieval

Code :

when RULE_INIT {
 
  ###
  # credentials required to access iControl REST API
  ###
 
  set static::adm_user "admin"
  set static::adm_pwd "admin"
 
  ###
  # settings required to authenticate the user trying to register an application
  ###
 
  set static::timeout 300
  set static::lifetime 300
  set static::access_profile "/Common/ap-ldap-auth"
 
  ###
  # settings required to update the APM configuration with the newly created ClientApp configuraiton
  ###
 
  set static::adm_partition "Common"
  set static::oauth_profile "my-oauth-profile"
  set static::scopes "myscope"
 
  ###
  # settings required to sync the OAuth 2.0 Authorization Server access profile
  ###
 
  set static::oauth_access_policy "ap-oauth-auth-server"
 
  ###
  # settings required to publish the client registration service
  ###
 
  set static::client_register_uri "/f5-oauth2/v1/client-register"
  set static::host "oauthas.example.com"

  ###
  # Define a static client_id and client_secret
  ###

  set static::use_static_app 1
  set static::client_id "xxxxxxxxxxxxxxxxxxxxxxx"
  set static::client_secret "xxxxxxxxxxxxxxxxxxx"
 
}
 
when CLIENT_ACCEPTED {
  ###
  # When we accept a connection, create an Access session and save the session ID.
  ###
 
  set flow_sid [ACCESS::session create -timeout $static::timeout -lifetime $static::lifetime]
}
 
when HTTP_REQUEST {
 
  ###
  # initialize vars
  ###
 
  set username ""
  set password ""
  set name ""
  set client_app ""
  set scopes ""
  set client_id ""
  set client_secret ""
  set agent ""
 
  set timestamp [clock seconds]
 
  switch -glob [string tolower [HTTP::header "User-Agent"]] {
    "*android*" { set agent "android" }
    "*ios*" { set agent "ios" }
    default { set agent "default" }
  }
 
  ###
  # identify client registration request. The client applicaiton needs to do a POST request on client registration URI and provides username and password
  ###
 
  if { [HTTP::path] eq $static::client_register_uri and [HTTP::host] eq $static::host and [HTTP::method] eq "POST" } {
    HTTP::collect [HTTP::header Content-Length]
  }
}
 
when HTTP_REQUEST_DATA {
  set username [URI::decode [URI::query "/?$payload" username]]
  set password [URI::decode [URI::query "/?$payload" password]]
 
  ###
  # play inline ACCESS policy to validate user credentials
  ###
 
  ACCESS::policy evaluate -sid $flow_sid -profile $static::access_profile session.logon.last.username $username session.logon.last.password $password session.server.landinguri [string tolower [HTTP::path]]
 
  if { [ACCESS::policy result -sid $flow_sid] eq "deny" or [ACCESS::policy result -sid $flow_sid] eq "not_started" } {
    HTTP::respond 403 content "{\"error\": \"Invalid user credentials\",\"error-message\": \"Access denied by Acces policy\"}" noserver Content-Type "application/json" Connection Close
    ACCESS::session remove -sid $flow_sid
    event disable all
  }
 
  ACCESS::session remove -sid $flow_sid

    if { !$static::use_static_app } {
 
      ###
      # generate client name and client application name
      ###
   
      set username [string map -nocase { "@" "." } $username]
   
      set name "$username-$agent-$timestamp"
      set client_app $name
      set scopes $static::scopes
   
      ###
      #   prepare and execute API REST call to create a new client application. Endpoint: /mgmt/tm/apm/oauth/oauth-client-app
      ###
   
      set json_body "{\"name\": \"$name\",\"appName\": \"$client_app\",\"authType\": \"secret\",\"grantPassword\": \"enabled\",\"scopes\": \"$scopes\"}"
      set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/oauth/oauth-client-app" -method POST -body $json_body -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd ]
      set json_result [call /Common/sys-exec::json2dict $rbody]
   
      if { $status contains "200" } {
   
        ###
        # extract client_id and client_secret from JSON body
        ###
   
        set client_id [lindex $json_result [expr {[lsearch $json_result "clientId"]+1}]]
        set client_secret [lindex $json_result [expr {[lsearch $json_result "clientSecret"]+1}]]
   
        ###
        # prepare and execute API REST call to bind the client application to the OAuth profile. Endpoint: /mgmt/tm/apm/profile/oauth/~$static::adm_parition~$static::oauth_profile/client-apps
        ###
   
        set json_body "{\"name\": \"$name\"}"
        set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/profile/oauth/~$static::adm_partition~$static::oauth_profile/client-apps" -method POST -body $json_body  -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd ]
        set json_result [call /Common/sys-exec::json2dict $rbody]
   
        ###
        # if binding is successful, respond to the client with client_id and client_secret
        ###
   
        if { $status contains "200" } {
   
          ###
          # Prepare and execute API REST call to apply Access Profile after Client Application has been assigned to OAuth profile
          ###
   
          set json_body "{\"generationAction\": \"increment\"}"
          set status [call /Common/HSSR::http_req -state hstate -uri "http://127.0.0.1:8100/mgmt/tm/apm/profile/access/~$static::adm_partition~$static::oauth_access_policy" -method PATCH -body $json_body  -type "application/json; charset=utf-8" -rbody rbody -userid $static::adm_user -passwd $static::adm_pwd ]
          set json_result [call /Common/sys-exec::json2dict $rbody]
   
          if { $status contains "200" } {
            HTTP::respond 200 content "{\"client_id\": \"$client_id\",\"client_secret\": \"$client_secret\"}" noserver Content-Type "application/json" Connection Close
            event disable all
          } else {
            HTTP::respond 403 content "{\"error\": \"Synchronization failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close
            event disable all
          }
        } else {
          HTTP::respond 403 content "{\"error\": \"ClientApp binding failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close
          event disable all
        }
      } else {
        HTTP::respond 403 content "{\"error\": \"ClientApp creation failed\",\"error-message\": \"[lindex $json_result 3]\"}" noserver Content-Type "application/json" Connection Close
        event disable all
      }
    } else {
      HTTP::respond 200 content "{\"client_id\": \"$static::client_id\",\"client_secret\": \"$static::client_secret\"}" noserver Content-Type "application/json" Connection Close
      event disable all
    }
}

Tested this on version:

13.0
Updated Jun 06, 2023
Version 2.0