CodeShare
Have some code. Share some code.
cancel
Showing results for 
Search instead for 
Did you mean: 

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
Comments
Herve_Bossant
Nimbostratus
Nimbostratus

Thanks for tyour contribution and your support. Nice piece of code.

 

Or_Yaacov
Legacy Employee
Legacy Employee

Nice Code Yann

 

In addition, 13.1.0.8 will introduce templates registration for client applications through the guided configuration framework. Your automation code is superb. Thanks for posting

 

Walter_Kacynski
Cirrostratus
Cirrostratus

I can find no code for the guided configuration. I have opened a support case to inquire on it's location.

 

Walter_Kacynski
Cirrostratus
Cirrostratus

Support says that Guided configuration is not yet available.

 

Or_Yaacov
Legacy Employee
Legacy Employee

@Walter Wacynski , Note AGC are available from 13.1.0.8 while re-licensing your APM

 

Walter_Kacynski
Cirrostratus
Cirrostratus

Thanks, I have it available now... but not on LAB licenses 😞

 

ShawnR
Nimbostratus
Nimbostratus

Are there any plans to support OID DCR from the official spec?

 

https://openid.net/specs/openid-connect-registration-1_0.html

 

Eelke
Altocumulus
Altocumulus

looks nice, will try to see if it works on v16

Version history
Last update:
‎22-Oct-2017 05:11
Updated by:
Contributors