Deep Diver – Azure AD Groups/Roles claims for developers and IT pro’s with code examples

Background

Many enterprise applications rely on group /role information to be passed on assertions for authorization, and further role decisions. Last three to five years these applications have been moving to the cloud, or at least seeing parts of their authorization middle-wares upgraded to support SAML, or OAuth2, or both. Judging by how rich the group claim options are in Azure AD I’d say Microsoft is investing heavily into making configuration options cover all imaginable scenarios

Short version of this blog is:

Prefer SAML when:

  • Application relies on getting user and groups information including transformed claims from the IDP in the initial token response.
  • Application doesn’t need further information from other Azure AD API’s to be acquired by background flows using token delegation
  • You want to transform claims both for user and group information in the GUI
  • You want do as much as possible configuration and maintanence in the GUI

Prefer OAuth2 when

  • Application has multiple Azure AD API’s called using token delegation, or redirect based flows after initial authentication
  • Application needs combine information from Azure AD API’s beyond of that which is held in user attributes and groups
  • While Claims Transformations arent supported in the token itself, you can basically do whatever you need to combine information once you have the initial token.
  • Application needs multiple and complex group claim rules and you can do these in the back-end (Azure AD allows only adding single ’add group claims’ rule in SAML app) – Note that even in OAuth2 this applies, but in OAuth2 you do this after receiving the initial token

Mix it together

  • In complex scenarios you might need to decide on combining a mix of these approaches, or decide to lean heavily towards OAuth2. As I am mostly working from dev perspective I tend to prefer OAuth2 with or without OIDC, but in this blog I highlight benefits for both approaches
    • ”For applications that do interactive browser-based sign-in to get a SAML assertion and then want to add access to an OAuth protected API (such as Microsoft Graph), you can make an OAuth request to get an access token for the API. When the browser is redirected to Azure AD to authenticate the user, the browser will pick up the session from the SAML sign-in and the user doesn’t need to enter their credentials.” /https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-saml-bearer-assertion

Useful information before proceeding

I’ve written few articles about Application Proxy If your applications remain on-premises but you need to modernize their access approach to Zero Trust

Eating the elephant

Since the scenarios where groups and roles are so many, I’ve tried to distill it into an table to help with possible evaluations, and provided some examples and considerations to back these scenarios.

Note that in Azure AD Group is not explicitly same as Role. But these two can be mixed in various rules together.

Decision criteria table

Send as claimsSAMLOAuth2
send roles 
send only groups that are assigned to the application ✔ (Send either the role or the group possible)✔ (Send either the role or the group possible)
send groups ✔(Via Group Claims in SSO settings of the enterprise application or Token configuration)✔ (Via ”Token Configuration OAuth2 Apps”)
Transform group/role attributes dynamically✔ (some limitations apply, but flexible nonetheless)See Query Graph API if when send as claims not possible
group size exceededSee Query Graph API if when send as claims not possible
or limit groups to those assigned to the application (preferred when full group information is not needed)
See Query Graph API if when send as claims not possible

Excerpt of configuration options presented in the table

send only groups that are assigned to the application (Via token configuration in App Registrations)

  • App Registrations Token Configuration includes three type of token configurations for the group claims option
  • Note the limitation only single group claims rule is available

send only groups that are assigned to the application (Via SSO settings in enterprise applications)

  • For SAML apps this approach is preferable when you also need to change the name of the claim
  • Note the limitation only single group claims rule is available

Query Graph API when send as claims is not possible

Querying Graph API is useful approach when you want/need information which is not available in the token response

Flows available for further queries

MethodSAML*OAuth2
Delegate the token received in response using bearer flow Only works for SAML 1.0 tokens Rather use OAuth2 redirect based flowJWT Bearer flow available
Redirect user for OAuth2 Authorization Request *✔ (SAML app needs to support OAuth2 to in this approach as well)
  • ”For applications that do interactive browser-based sign-in to get a SAML assertion and then want to add access to an OAuth protected API (such as Microsoft Graph), you can make an OAuth request to get an access token for the API. When the browser is redirected to Azure AD to authenticate the user, the browser will pick up the session from the SAML sign-in and the user doesn’t need to enter their credentials.” /https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-saml-bearer-assertion
  • For SAML and JWT tokens which exceed group size limit you get Graph API link instead of the groups
  • Azure Active Directory limits the number of groups it will emit in a token to 150 for SAML assertions, and 200 for JWT. If a user is a member of a larger number of groups, the groups are omitted and a link to the Graph endpoint to obtain group information is included instead.
  • In this approach you need to add additional permissions for the application if you want to get the group names besides group id’s.
    • If you emit groups in the claims of the token you get with the built-in scope to graph the needed information

https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-fed-group-claims#options-for-applications-to-consume-group-information

SAML / OAuth2 group information examples

In attempt to keep this blog even remotely readable I’ve included examples of the two main approaches, as all other examples are more or less derivatives/mixes of those two

SAML

  • deliver most if not all information in the token response

OAuth2

  • fetch information after receiving the initial token via separate http request to MS Graph API

Emit Groups in the token (works for both SAML and OAuth2)

Configuration for the examples below

  • Note, here is alot to play with for different group configurations

Response example JWT token (Access Token):

OAUTH2
Though avoidable, some times you end up getting group IDs only. If you prefer this way, and don’t need human readable name to be exposed in token this may work too. If you want name of the group, you need to use graph API

Response example SAML token

Response example for exceeded group amount

Get groups of the user after token response – OAuth2 (And SAML APPS that support OAuth2)

  • This scenario could be needed if filtering is required that is not available in claims customization, or the group size exceed token limits

Using OAuth2 JWT Bearer Flow

  • I’ve written about JWT bearer grant earlier here Concept: Publish on-prem API using AAD App Proxy and API Management with Azure AD JWT Bearer Grant
  • Note that this can be also achieved with:
    • Getting new access token with interactive user redirect flow
    • using refresh token flow (if initial scope allows storing refresh tokens)
    • SAML Bearer flow (This works with only SAML 1.1 tokens, so I am not recommending it, unless its token gotten from WSfederation protocol)
  • The JWT token is stored either in back-end token store, or in user cookies (In the example is in the req.cookies.token)

Response example

Note! you can clean up the contents of graph response, if needed. This is just example 🙂

Sample code for JWT-Bearer Grant

//Bearer Grant (depedencies request etc)

var getJwtBearerAssertion = ({client_id,redirect_uri,resource,assertion,client_secret},callback) => {
  //console.log(secret)
    var options = {
      json:true,
      headers:[{
      "content-type":"application/x-www-form-urlencoded" 
      }
      ],
      form: {
          grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",
          client_id,
          redirect_uri,
          resource,   
          assertion,
          "requested_token_use":"on_behalf_of",
          client_secret
          }
      }
  
    rq.post("https://login.microsoftonline.com/common/oauth2/token",options, (error,response) => {
     //console.log('reqiest',options)
  
        if (!response.body.access_token) { return callback(undefined,response.body)}
  
       fs.writeFileSync('token.JSON',JSON.stringify(response.body))
        callback(response.body.access_token, undefined)
     }
    )
  
  }

// New code from here /Different JS file
var options = {
                client_id:ClientId,
                redirect_uri:RedirectUri,
                resource:"https://graph.microsoft.com",
                assertion:req.cookies.token,
                "client_secret":secret
             }
 
   getJwtBearerAssertion(options, (result,error) =>  {
                if (error) {return res.send(error)}
                apiCall(result,"https://graph.microsoft.com/v1.0/me/memberOf", (result) => {
 
                      var data =   result.value.filter((group) => {
                          console.log('group iterated')
//Iterate groups into Cookies
                         if (group.displayName) {res.cookie(`groupID:${group.id}`,group.displayName,CookieOpts)}
                        return group.id != undefined
                      })
                      
                      res.send(data)
                            }
                    )
                 

Other stuff: SAML parser in NodeJS

Following parsing code is used as expressJS middleware, to implement demonstrational SAML parsing functionality without verifying the SAML token itself. This is useful when you want to inspect assertions in the back-end test that information on functions you might be working. Such as updating back-end database stored information based on the information of such assertion

SAML parser Middle-ware function for expressJS

var xmlparser = require('fast-xml-parser');
const util = require('util')

function samlParser () {

    return function (req,res,next) {
       
        if (req.body.SAMLResponse) {
            console.log('SAML response')
            let bbuffer = Buffer.from(req.body.SAMLResponse, 'base64')
            var xmlstring = bbuffer.toString('utf-8')

            if (xmlparser.validate(xmlstring)) {
                
                var options = {
                ignoreAttributes:false
                }
            var xmlpayload = xmlparser.parse(xmlstring,options)
            var detailed = util.inspect(xmlpayload,true,7,true)
            console.log(detailed)

            req.body={'token':req.body.SAMLResponse}

            return next()
                
            }
            
        }

        next()
    }

}
console.log('using SAML parser')

module.exports={samlParser}

Security aspects

Using app permissions in place of delegated permissions?

This is generally not recommended practice (at least in my opinion) because

Using app permissions for substituting flows that are originally suited to use user delegation is not good idea, because it decouples user authorization and further requests to other API’s from the user context.

With delegated permissions 

  • With delegation existing access rights of the user is used to get further information. The process delegating the token cannot exceed permissions of the user with the token
  • More coherent log trail is produced as user context is shown which the app accessed API’s on behalf of the user

When instead app permissions are used

  • Log trail is problematic. The action which was destined towards user API’s doesn’t show user context rather only the application acting towards the API
  • Elevation of privilege risks enter the picture, as the app permissions might exceed by far margin the permissions of the user
  • incomplete or no user consent even when the app does actions on behalf of user without delegation. 

Attacker figuring out a client side input parameters intended for back-end queries

  • In ”SAML approach” all of the delivered attributes are verified by signature verification, this applies to OAuth2 tokens too, but in OAuth2 approach you need to query often information after receiving the token for further information
    • Since the token JWT token might not have needed information, hypothetically you could have bad application code running ”curl –insecure-” against what it expects to be Graph API” – but is instead being tricked to read an attacker controlled version of the endpoint. Attacker could use an limited Remote Code Injection approach, or figure out that some client side information is used in the back end with no integrity check, or has serious issues in with its input validation
      • example function running in the back-end expects that the array from client side function always produces single item array. The validation function assumes that the content is thus always stored in the index[0]. But the function processes all items in the array if the first index item is validated
  • The obvious counter argument here is that if application is running insecure curl requests and such bad coding practices 🙂 how can you be sure, that its not being tricked to use the wrong token sign-in key? or have more serious security issues. In especially older apps, the public verification key is stored in the application itself With Azure AD this is not recommended, querying the JWKS uri needs to happen always from the metadata because those public keys can rollover (Read ”Signing key rollover in Azure Active Directory”)
    • There is no plausible production scenario where you wouldn’t be verifying signatures of the either token types (SAML / JWT)

Where from here?

The current blog highlights technical decision criteria’s and examples of code sample and response outputs. For solving a particular group based scenarios don’t hesitate to ask me in twitter or LI for further additions into this blog

Microsoft references

https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims

https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-fed-group-claims

Advisories 1-2: Azure AD and Common WS-Trust MFA Bypass explained

If there is single post I’ve been delaying for a good while, then it’s gotta be this one. This blog details a common oversight in MFA enforcement regarding federation implementations where MFA is invoked and required in the 3rd party IDP only. This oversight become effective when several by- design features, and implementation decisions align in ”wrong” way

Well how actionable it is? I’ve been exploiting this oversight in Red Teaming assignments, so in my experience its been really popular in deployments where the MFA is not checked on cloud side.

The reason of the delay is advisory 1. I’ve been discussing the ramifications with a major vendor providing Federation solutions about mitigating the configuration risks with WS-Trust when used in conjunction with Azure AD

Advisory 1 is related to how WS-Trust protocol is used in conjunction with Azure AD where clear text username and password are forwarded in TLS protected transport using the active endpoints to provide access for so-called non modern clients (Rich /Active endpoint consuming clients)

The scope is protocol implementation related, not vendor related! List of third 3rd party vendors providing WS-Federation/Trust support for Azure AD is long (some 15 last time I checked), so I am not highlighting any vendors here. I am assuming this affects most of the vendors implementing the WS-Trust with Azure AD without knowledge of the Azure AD OAuth2 Grant Types. Obviously I can’t say anything on any of the vendors behalf.

Microsoft’s AD FS can be also considered applicable in this context. Especially advisory 2

Advisory 2 is already documented, but not well communicated, and the existing documentation is AD FS specific. How is advisory 2 related to advisory 1? The existence of advisory 2 makes advisory 1 possible in the described scope.

Scope

Scope of this advisory are primarily customers who use WS /* -Protocols for federated domains in Azure AD, and utilize access policies to enforce and bypass MFA only in the IDP side. Not checking the status of MFA in Conditional Access, or using the -SupportsMFA option for the Microsoft MFA enabled users.

Its worth noting, that the attack can also pierce cloud MFA requirement if EAS clients can generate known bypass claims. There are unfortunately guides on web that advise doing this. In the latter scenario the attack penetrates two MFA layers, (Becomes even worse)

I won’t be providing any PoC’s, since all of the below advisories rely on by-design implementation, and can be replicated in few minutes, given you have understanding of both sides of the equation (Azure AD and WS-protocols)

Advisory 1 – Pivoting from legacy to modern auth

Background

UserName endpoints on WS-Trust are usually associated with legacy clients, thus they are often excluded from MFA. These endpoints are usually associated with legacy clients requesting access to Azure AD relying Party (Office 365 services often) – This is only true by association, as WS-Specification doesn’t define any difference between the so-called modern and legacy clients, the concept of difference only exists within Azure AD /Office 365 context, which the federation service is not aware of.

Accessing ”modern” API’s unprotected by the IDP

SAML assertion can extracted from the IDP, if the IDP is thinking it’s ActiveSync client. Once the token is received in response, it is then converted to OAuth2 Access Token with “saml1_1-bearer” OAuth2 Grant. This can be done by using many of the existing public clients.

Effectively the SAML token destined to legacy endpoint in Azure AD is converted to “Modern Auth” OAuth2 Access Token, and not being just used for legacy protocols anymore.

Root of this problem is related to the fact, that for SAML response from WS-Trust endpoint there is no ”validFor” context, which would dictate, that this request is valid for only let’s say https://graph.microsoft.com – not entire Azure AD Federation (Conditional Access solves this issue, as it binds access in cloud to different API’s)

In the picture: MFA is enabled for browser clients, and UserNameEndpoint is only allowed for ActiveSync clients without MFA /There lies the problem…

But wait, I have disabled legacy protocols in Office 365?

Disabling legacy protocols doesn’t help in this case, as the attacker is only consuming “modern API’s” (Microsoft OAuth2 parlance).

Attack in practice – On-prem MFA bypass

Example: The deployment only allows native ActiveSync to access WS-Trust’s Username bindings without MFA

What attacker does: Attacker uses ‘X-MS-Application’ header to spoof client to fetch SAML token using WS-Trust’s ’UserNameWSTrustBinding’.

The attacker then exchanges the SAML token in Azure AD to access the OAuth2 SPN (Audience). The exchange of the token is possible due to explicit support of “saml1_1-bearer” oAuth2 Grant Type, which cant be disabled on any of the ServicePrincipals in AzureAD

Documentation regarding the flow type

Link Microsoft identity platform and OAuth 2.0 SAML bearer assertion flow

Advisory 2 – Client Header Based Access Policies (Applies to WS-Federate as well)

This advisory is well documented, and is here only as reminder because it allows the first advisory to exist.

Don’t use user spoof-able headers as an basis for access policies at IDP’s controlling access to Azure AD.

Access Policies

Some vendors might implement additional checks here, but the protocol itself doesn’t dictate those to be done. This is basically where different products provide access policies to be enforced claims pipeline, to produce authorization result.

AD FS roles
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/technical-reference/the-role-of-the-claims-pipeline

Business policy claims?

Some of the claims presented in the following category should be only implemented to support business policies, not security policies, as advised in the Microsoft’s article

Client Spoof-able headers (Examples from AD FS)

Multiple claims exist which clients can modify, or set. Below are the most common ones. Read the article if you’re curious to see the list of claims in AD FS

  • X-MS-FORWARDED-CLIENT-IP
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#x-ms-forwarded-client-ip
  • X-MS-CLIENT-APPLICATION
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#x-ms-client-application
https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/access-control-policies-w2k12#scenario3

The documentation exist for AD FS 2012 R2, but it’s applicable to similar implementations including subsequent AD FS versions at least as up to 2016. Be sure to read the article as it lists very clear reasons,

Mitigations

  • a. Use Conditional Access to check whether MFA was actually performed at the 3rd party IDP for the requested API, or if there is valid reason to explicitly bypass the API
  • b. If you don’t have Conditional Access, don’t allow UserNameMixed to be used with spoof-able headers. Basically you wouldn’t allow native clients anymore for ActiveSync, as there isn’t safe header available for this decision to be made in the authorization pipeline

About spoof-able headers

  • X-MS-ADFS-Proxy-Client-IP seems to be based on the actual client IP with WAP, and should be more reliable, than the client writable header X-MS-FORWARDED-CLIENT-IP, which is advised to be used in many scenarios.
  • X-MS-FORWARDED-CLIENT-IP is often advised to be used in many guides because MS Exchange Online Connections pass multiple IP’s in headers, and thus the actual client IP hitting ADFS includes both Exchange and the Client IP’s in the header
    • as the documentation says, is best effort style header, and shouldn’t be used outside of business logic use scenarios
  • X-MS-CLIENT-APPLICATION here isn’t reliable way to inspect ActiveSync header for a legitimate client at the IDP’s side, The only way to allow it without MFA is Conditional Access

Also its worth noting that Basic Auth for Exchange, which is key reason why MFA bypasses on the WS-Trust endpoint are still used is going to be deprecated very soon