oauth
4 TopicsBypass Azure Login Page with OAuth login_hint on F5 BIG-IP APM
Overview This article demonstrates how to enhance the user experience between F5 BIG-IP APM (OAuth Client) and Azure AD (OAuth Authorization Server) by implementing the login_hint parameter. This approach eliminates the need for users to enter their credentials twice, once on the F5 login page and again on the Azure login page. Problem Statement When users access applications protected by F5 APM that authenticate against Azure AD using OAuth/OIDC, they often encounter a suboptimal experience: User enters credentials on the F5 login page (often using their familiar sAMAccountName) F5 redirects to Azure AD for OAuth authentication Azure AD presents another login page asking for the same credentials (expecting UPN/email format) This creates additional friction because users may not know their User Principal Name (UPN) and typically use their sAMAccountName for domain authentication. The login_hint parameter solves this by allowing F5 APM to translate the user's sAMAccountName to their UPN and pre-populate the Azure login page, effectively bypassing the redundant login prompt. Alternative approach The same functionality can be achieved using SAML, as described here. However, I find the OAuth approach cleaner, as it eliminates the need for an iRule and also allows you to include the prompt parameter. This can be useful when you want to force authentication in Azure instead of relying on an existing session. See herefor details. Prerequisites F5 BIG-IP with APM Azure AD tenant with application registration Basic OAuth/OIDC configuration between F5 and Azure AD (as outlined in https://my.f5.com/manage/s/article/K53313351) Implementation Steps Step 1: Complete Basic OAuth Setup Follow the standard F5 APM OAuth configuration with Azure AD: Configure OAuth Server Object Navigate to Access ›› Federation ›› OAuth Client/Resource Server ›› OAuth Server Create a new OAuth server configuration for Azure AD Set the provider type to "Microsoft Identity Platform 2.0" Configure Access Profile Create an access profile with OAuth Client agent Configure authentication redirect and token requests Step 2: Create Custom Authentication Redirect Request The key modification involves creating a custom authentication redirect request that includes the login_hint parameter: Navigate to Request Configuration Access ›› Federation ›› OAuth Client/Resource Server ›› Request Clone the Default Request Find the existing /Common/MSIdentityPlatform2.0AuthRedirectRequest Create a copy Name it something descriptive like MSIdentityPlatform2.0AuthRedirectRequest_LoginHint and the same parameters as /Common/MSIdentityPlatform2.0AuthRedirectRequest Add login_hint Parameter Parameter Name: login_hint Parameter Type: custom Parameter Value: %{session.ad.last.attr.userPrincipalName} or any other variable containing user’s UPN based on your environment and Authentication configuration In the Parameters section, click Add Configure the new parameter: Step 3: Update VPE OAuth Configuration Access the Visual Policy Editor Go to Access ›› Profiles/Policies ›› Access Profiles (Per-Session Policies) Edit your access profile Click Edit to open the VPE Modify OAuth Client Agent Locate the OAuth Client agent in your policy Edit the OAuth Client agent properties In the Authentication Redirect Request dropdown, select your new custom request: MSIdentityPlatform2.0AuthRedirectRequest_LoginHint Apply Access Policy Click Apply Access Policy to save changes Step 4: Session Variable Configuration and UPN Translation The key to successful login_hint implementation is ensuring the proper session variable is populated with a UPN-formatted email address. F5 APM can automatically translate sAMAccountName to UPN during AD authentication. Complete Policy Flow Example Start ↓ Logon Page (user enters: DOMAIN\john.doe or john.doe) ↓ AD Auth (authenticate and populate session variables) ↓ AD Query (to fetch user attributes) ↓ OAuth Client (with custom redirect request including login_hint=john.doe@company.com) ↓ Azure AD (pre-populated with UPN, bypasses login prompt) ↓ Application Access Troubleshooting Common Issues login_hint Parameter Not Working Verify the session variable contains a valid email address Check that the custom request is selected in the OAuth Client agent Ensure the parameter type is set to "custom" Session Variable Empty or Wrong Format Verify AD authentication and query occurs before OAuth redirect Check AD attribute mapping configuration Confirm the userPrincipalName attribute exists in your AD schema Ensure domain suffix matches Azure AD tenant domain Azure AD Still Prompts for Login Verify the email format matches Azure AD expectations Check Azure AD application configuration for login_hint support Ensure the user exists in the Azure AD tenant Debugging Tips Enable APM debug logging: tmsh modify sys db log.apm.level value debug Use browser developer tools to inspect the OAuth redirect URL Verify session variables using Variable Assign agents in VPE Security Considerations The login_hint parameter only pre-populates the username field; users still must provide valid credentials This is a user experience enhancement, not a security bypass Ensure session variables don't contain sensitive information beyond the username/email Conclusion Implementing login_hint with OAuth on F5 BIG-IP APM significantly improves user experience by eliminating redundant login prompts. The key advantage is that F5 APM can seamlessly translate users' familiar sAMAccountName credentials to the UPN format required by Azure AD, allowing users to authenticate once with their domain credentials while Azure AD receives the properly formatted UPN for the login_hint. This approach maintains security while providing a seamless user experience, particularly beneficial in environments where: Users are more familiar with their sAMAccountName than their UPN Organizations want to minimize authentication friction Azure itself is also federated to another IDP and you want transparent rederiction The solution leverages F5 APM's AD integration capabilities to handle the username format translation automatically, making it transparent to end users.244Views3likes2CommentsOAuth 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.01.3KViews0likes8CommentsRate Limiting based on ACCESS TOKEN (OAuth 2.0)
Problem this snippet solves: When publishing web services, you need to implement some rate limiting functions to avoid abuses. There are plenty of ways to setup Rate limiting How to use this snippet: The code below setup a rate limiting based on the ACCESS TOKEN. The client will receive a response "429 Too much requests" after 1000 requests in a window of 300 seconds. The client can request its current status by doing a request to /rate_limit_status. He will then receive the following JSON message : { "x-rate-limit-limit": 1000, "x-rate-limit-remaining": 800, "x-rate-limit-reset": 100 } Code : when RULE_INIT { ### # rate limit options ### set static::request_limit 1000 set static::window_size 300 ### # define URI endpoints ### set static::status_uri "/rate_limit_status" } when HTTP_REQUEST { ### # initialize vars ### set access_token "" set client_ip "" ### # retrieve the access_token. It will be used as a mandatory key to evaluate rate limiting ### if { [HTTP::header exists Authorization] and [HTTP::header Authorization] contains "Bearer" } { set access_token [getfield [HTTP::header Authorization] " " 2] set client_ip [IP::client_addr] } if { !($access_token eq "") } { ### # provide client with rate limit status ### set key [sha1 $access_token] set count [table lookup -notouch $key] set time [table timeout -remaining $key] ### # Provide a status page to the client ### if { [HTTP::path] eq $static::status_uri and [HTTP::method] eq "GET" } { if { $count > 0 } { set x_rate_limit_limit "$static::request_limit" set x_rate_limit_remaining "[expr {$static::request_limit-$count}]" set x_rate_limit_reset "$time" } else { set x_rate_limit_limit "$static::request_limit" set x_rate_limit_remaining "$static::request_limit" set x_rate_limit_reset "$static::window_size" } HTTP::respond 200 content "{\"x-rate-limit-limit\": $x_rate_limit_limit,\"x-rate-limit-remaining\": $x_rate_limit_remaining,\"x-rate-limit-reset\": $x_rate_limit_reset}" noserver Content-Type "application/json" Connection Close event disable all } else { ### # Handle the case where a client reach the rate limit ### if { $count >= $static::request_limit } { set x_rate_limit_limit "$static::request_limit" set x_rate_limit_remaining "0" set x_rate_limit_reset "$time" HTTP::respond 429 content "{\"x-rate-limit-limit\": $x_rate_limit_limit,\"x-rate-limit-remaining\": $x_rate_limit_remaining,\"x-rate-limit-reset\": $x_rate_limit_reset}" noserver Content-Type "application/json" Connection Close event disable all } else { if { $count == 0 } { table add $key 1 $static::window_size $static::window_size } else { table incr -notouch $key } } } } } Tested this on version: 11.5823Views0likes1CommentJSON Web Token (JWT) Parser
Problem this snippet solves: This feature is now native in v13.1 and it is strongly recommended you implement the native solution instead. This code is left only as an example for future use cases, it should not be used for JWT handling because there is no signature validation. This code parses a JWT (JSON Web Token) received by a Big-IP acting as an OAuth client and creates session variables for the JSON parameters in the header and payload. Example use cases might include Azure AD B2C or Azure AD Enterprise integration. This iRule does not perform signature validation. Code from the "Parse and Set Session Variables" section down could be easily harvested for other JSON parsing use cases that do not need the JWT decoding. How to use this snippet: Attach this iRule to the virtual server receiving the JWT that is configured for OAuth. Inside the VPE after the OAuth Client agent add an iRule agent with id jwt-parse. This iRule will set several variables including: session.oauth.jwt.last.header session.oauth.jwt.last.payload session.oauth.jwt.last.signature In addition it will create a session variable for each parameter in the header and payload in the following syntax. session.oauth.jwt.header.last.* session.oauth.jwt.payload.last.* You can then call these session variables elsewhere. Code : when ACCESS_POLICY_AGENT_EVENT { if { [ACCESS::policy agent_id] eq "jwt-parse" } { #log local0. "JWT-Parse: Started" #Get the JWT set jwt [ACCESS::session data get -secure session.oauth.client.last.id_token] #log local0. "JWT-Parse: JWT Received - jwt is $jwt" #Separate the header, payload, and signature set jwt_header [getfield $jwt "." 1] ACCESS::session data set session.oauth.jwt.last.header $jwt_header #log local0. "JWT-Parse: Header extracted - jwt_header and session.oauth.jwt.last.header are $jwt_header" set jwt_payload [getfield $jwt "." 2] ACCESS::session data set session.oauth.jwt.last.payload $jwt_payload #log local0. "JWT-Parse: Payload extracted - jwt_payload and session.oauth.jwt.last.payload are $jwt_payload" set jwt_signature [getfield $jwt "." 3] ACCESS::session data set session.oauth.jwt.last.signature $jwt_signature #log local0. "JWT-Parse: Signature extracted - jwt_signature and session.oauth.jwt.last.signature are $jwt_signature" #Base 64 decode the header and payload #Fix encoding issues in header set jwt_header_modified $jwt_header set tail [string length $jwt_header_modified] if {$tail % 4 == 2} { append jwt_header_modified {==} } elseif {$tail % 4 == 3} { append jwt_header_modified {=} } #log local0. "JWT-Parse: Header encoding fixes complete - jwt_header_modified is $jwt_header_modified" #Fix encoding issues in payload set jwt_payload_modified $jwt_payload set tail [string length $jwt_payload_modified] if {$tail % 4 == 2} { append jwt_payload_modified {==} } elseif {$tail % 4 == 3} { append jwt_payload_modified {=} } #log local0. "JWT-Parse: Payload encoding fixes complete - jwt_payload_modified is $jwt_payload_modified" #Base64 decode set jwt_header_modified [b64decode $jwt_header_modified] #log local0. "JWT-Parse: Header Base 64 decoded - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [b64decode $jwt_payload_modified] #log local0. "JWT-Parse: Payload Base 64 decoded - jwt_payload_modified is $jwt_payload_modified" #Parse and Set Session Variables #Remove JSON characters set jwt_header_modified [string map {\{ {} \} {} \[ {} \] {} \" {}} $jwt_header_modified] #log local0. "JWT-Parse: Header JSON Characters removed - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [string map {\{ {} \} {} \[ {} \] {} \" {}} $jwt_payload_modified] #log local0. "JWT-Parse: Payload JSON Characters removed - jwt_payload_modified is $jwt_payload_modified" #Split into field/value pairs set jwt_header_modified [split $jwt_header_modified ,] #log local0. "JWT-Parse: Header Fields split - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [split $jwt_payload_modified ,] #log local0. "JWT-Parse: Payload Fields split - jwt_payload_modified is $jwt_payload_modified" #Set APM session variables for each header parameter foreach parameter $jwt_header_modified { set variable_name [getfield $parameter ":" 1] set variable_value [getfield $parameter ":" 2] ACCESS::session data set session.oauth.jwt.header.last.$variable_name $variable_value #log local0. "JWT-Parse: Header session variable set - session.oauth.jwt.header.last.$variable_name is $variable_value" } #Set APM session variables for each payload parameter foreach parameter $jwt_payload_modified { set variable_name [getfield $parameter ":" 1] set variable_value [getfield $parameter ":" 2] ACCESS::session data set session.oauth.jwt.payload.last.$variable_name $variable_value #log local0. "JWT-Parse: Payload session variable set - session.oauth.jwt.payload.last.$variable_name is $variable_value" } } } Tested this on version: 13.03.9KViews2likes14Comments