
This article covers the end-to-end tasks for deploying and enabling an Okta OIDC supported HashiCorp Vault integration backed with Microsoft Active Directory group memberships.
This is a quite long and intensive blog post and isn’t intended for the casual reader. If you want to know whether VAULT supports OIDC and OKTA verify number challenges then the answer is YES!!! You may stop reading now…..if you want to understand …. then…..
Everywhere I looked, from HashiCorp, OpenIDC, oAuth, Microsoft and Okta, they all were lacking in a detailed description on how the pieces of the puzzle fit together. I didn’t want to just “hand of” some parameters about my app and plug in some URLs and secret keys from the Idp, I wanted to know how the whole thing is built.
Disclaimer: This is serious stuff. This blog post is not advocating or encouraging the use of this authentication method over another. This is a passion project. There may be bugs or vulnerabilities in this blog and in the vendor’s implementations that I cannot be responsible for. Please review the source code and admin guides yourself and pen-test the solution if you’re going into production. I’ve provided source code for your own review.
The tutorial is organized in a the following sections. If you only care about the “Vault” configuration side then you can skip way ahead.
- Active Directory Deployment
- Okta Active Directory integration
- Okta – Authorization Server Set-Up
- VAULT – OIDC – Authentication Method
- VAULT – OIDC – Authorization
- OKTA – MFA & BEHAVIOR DETECTION
- VAULT – OPEN SECURITY ISSUES

Active Directory Deployment

First start I started by deploying a simple Windows server running a Microsoft Active Directory role and DNS role. I don’t want to spend too much time here so I’ll boil this down quickly.
The intent is to replicate a similar use-case that many corporations have which is to integrate on-premise active directory with Okta SSO provider.
You can deploy a Windows server manually or via your automated means. For example you can use the Terraform AWS provider or AWS Cloud formation templates. I wont evangelize either to you.
Here is a snippet of a Windows-Laucher which deploys windows in an isolated security group and encrypts the root volume.
https://github.com/secSandman/vault-oidc-setup/blob/main/windows-launcher.yaml
I created an Active Directory group called vault-oidc-1, although in your environment you may support Tenant namespace and follow a more strict naming convention such as vault-tenant-role

Then I created some mock users and added them to the vault-oidc-1 group memberships.


Logically, things are looking pretty simple at the moment… We have a windows server running AD, with a simple group and a few users sitting behind AWS security group and native windows defender firewall. Eventually, we’ll want to extend this AD server as the trusted provider for the Okta and Vault Auth flows.

Okta Active Directory integration
Next step is to deploy and configure your own Okta organization.
You can sign up for an Okta developer edition org for free, which allows for up to 100 monthly active users (MAU). Later, when you need more capacity, you can upgrade to a paid org.
To start, you’re going to need an org. An org is a private data space — provided by Okta — that holds all the resources that you create to handle user authentication. See Okta organizations.
Okta organizations are logical boundaries that essentially represent your company’s core domain space. For example, securitysandman.com would be an Okta organization.
Okta gives you a generic organization name but you may also add your own custom organization name tied to a domain you own. As you’d expect, you update your DNS provider and add some TXT records to validate your ownership before they allow you to do this, otherwise this would be an avenue for MiTM attack channels by virtue of hijacking auth flows. Additionally, Okta dev throttles the access attempts to mitigate DoS and brute force by default.

Now, we need to integrate our active directory with Okta so that user’s can authenticate against a trusted source and so that Okta can access the user’s profile and attributes to be used in the oAuth and OIDC flows.

Okta, enables this with the Okta-AD agent, the agent installation, it’s permissions and service account permissions and it’s attack vectors is a few chapters of study by itself. For this PoC, we’ll just perform a basic installation using default settings but be warned this is a serious security step and should taken seriously. Installing the wrong binary, too high permissions, wrong network acls or registering the wrong organization could compromise your entire company.
Okta recommends you run the AD Agent outside of the domain controller on a windows domain joined server. Logically, like so …

A few notes when doing this in a brand new AD PoC installation …
- You’ll need to add your users to an RDP group to access your windows servers, by default you cannot RDP into these systems because you don’t have permission
- You’ll need to update the default AD GPO before adding the okta agent windows server
- Update the GPO to allow remote administration and update the windows firewall settings
- Leave the other defaults in place such windows defender threat protection enabled, windows firewall enabled, password policies, encryption algorithms etc. etc.


Next: Install the Okta Agent per Okta’s best practices below
https://help.okta.com/en/prod/Content/Topics/Directory/ad-agent-prerequisites.htm
https://help.okta.com/en/prod/Content/Topics/Directory/ad-agent-known-issues.htm
NOTES:
- Use your organization id as the domain, avoid using the custom url as it throws errors
- If you get install errors, you may need to navigate to Program Files and delete the folder and it’s contents and attempt to re-install
Okta –> Directory –> Directory Integration –> Add Directory –> Install Agent

After install, you should see your domain integration ….

Navigate to Provisioning –> integration –> Scroll down to the bottom –> Delegated authentication
What is Okta Delegated Authentication?
When Okta is integrated with an Active Directory (AD) instance, delegated authentication is enabled by default. With delegated authentication, this is what happens when users sign in to Okta:
- Users enter their username and password in the Okta sign-in page. The sign-in page is protected with a security image to prevent phishing.
- Multifactor authentication (an extra security question or smart phone soft token) may also be enabled.
- The username and password are transmitted over the SSL connection implemented during setup to an Okta Active Directory (AD) Agent running behind a firewall.
- The Okta AD Agent passes the user credentials to the AD domain controller for authentication.
- The AD domain controller validates the username and password and uses the Okta AD Agent to return a yes or no response to Okta.
- A yes response confirms the user’s identity and they are authenticated and sent to their Okta homepage.
Delegated authentication maintains persistence for your directory authenticated (DelAuth) sessions and AD is maintained as the immediate and ultimate source for credential validation. As AD is responsible for authenticating users, changes to a user’s status (such as password changes or deactivations) are immediately pushed to Okta.
Test user user authentication ….


This means the core authentication functionality is working as expected for the OIDC PoC
Navigate to Okta –> Directory –> Directory Integration –> Directory –> Import –> Import Now –>

This will import the necessary user profile attributes and group memberships needed to support future OIDC authorization flows. You do not need to import all objects from the directory, although we’re jumping through this quickly, WARNING: if you import too much data you may unnecessarily expose sensitive data to a SaaS system and leak information or simply import too much and make a more complex environment to navigate.
Let’s validate if we successfully imported our groups and users
Navigate to Okta –> Directory –> Groups

Browse around the user and group objects and you’ll notice that those domain objects should have imported into Okta. This means that Okta and now use the objects and their attributes to create token assertions and claims to support authorization flows as oppose to every application needing direct access to active directory which is often hosted internally and not exposed to internet.
oauth and oidc refresher
Feel free to skip over this step, I’m just adding for good measure.
- oAuth is a framework authorization, meaning tokens used to say user:foobar is role/group xyz and you can trust this because it’s a signed token
- OpenIdc: Authentication built on top of OAuth 2.0 and the use of Claims to communicate information about the End-User
At the time of this writing, per oAuth “clients such as native apps and JavaScript apps should now use the authorization code flow with the PKCE extension instead”
You’ll notice mention in VAULT source code and in Okta configurations of the implicit grant type. Avoid this.
The OAuth 2.0 Security Best Current Practice document recommends against using the Implicit flow entirely
Scopes
Scope is a mechanism in OAuth 2.0 to limit an application’s access to a user’s account. An application can request one or more scopes, this information is then presented to the user in the consent screen, and the access token issued to the application will be limited to the scopes granted.
In Okta, this can be pre-configured, such that only certain scopes and certain claims can be included or not. For example, if your token does not include a particular scope, but your target application is expecting that scope, then the authorization will fail in the application.
Scopes are groups of claims. They provide a logical grouping of claims. A common example is the standard OpenID Connect scope profile
. Consenting to the use of this scope will result in getting an ID Token which will include the following claims: name
, family_name
, given_name
, middle_name
, nickname
, preferred_username
, profile_picture
, website
, gender
, birthdate
, zone_info
, locale
, updated_at
Claims
Claims provide you with information, and they are found in tokens. For example, an ID Token will consist of some claims with information about the user, maybe their first and last name, e-mail or address.
In our example, this can be various attributes email or groups membership information that we imported into Okta from our Active Directory.
PKCE
PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent CSRF and authorization code injection attacks.
Basically, this mechanism helps mitigate MiTM attacks and it always recommended.
CORE OIDC SPEC

https://openid.net/specs/openid-connect-core-1_0.html
https://datatracker.ietf.org/doc/html/rfc6749
Okta – Authorization Server Set-Up
Next, we need to set-up the authorization server in Okta….
Instead of using the resource owner's credentials to access protected resources, the client obtains an access token -- a string denoting a specific scope, lifetime, and other access attributes. Access tokens are issued to third-party clients by an authorization server with the approval of the resource owner. The client uses the access token to access the protected resources hosted by the resource server.
Navigate to Okta –> Security –> API –> Add Authorization Server
If you use the default authorization server, you may notice other issues pop up or extra work such as custom claims, fat token and audience.


You’ll want to do something similar to the above, choose either default issuer or custom url issuer depending on your situation…
Vault Authorization Server–> Scopes
Add group scopes, by default, the scope groups
is not automatically configured for custom authorization servers and needs to be manually added, together with a claim to retrieve the user’s group memberships.
Additionally, Okta is configured to only pass thin tocken during the authorization code flow exchange per oAuth specs…

Vault Authorization Server–> Claims
“For Custom Authorization Servers, you are able to create Custom Claims to store group membership, as described in this guide. If the claim you create is configured to “Always” be included in the ID Token (as shown in the screenshot below), you will not need to make a /userinfo call to get this claims”
You’ll need to create an expression to find the “groups” that you want to use in your application. As a side note, because okta groups can be created a number of ways, a lot of “blogs” and “tutorials” only demonstrate using “okta managed groups” using pattern “Groups” regex and search option and not the Okta expression option.
From, what I observed the “groups” option shown below doesn’t work with AD managed groups as a feature limitation meaning I had to use expressions…although I could be wrong …

The expression I’m using queries for both Active Directory and Okta managed groups.
However, I could not get the Groups.containt() expression to work and filter down to only “vault-” AD group names.
WARNING: This means that my current groups filter introduces an information disclosure vulnerability in the JWT which discloses too much group information in the token. If the token was stolen, “we’d have bigger problems” but we also don’t need extra data in it for for an attacker to enumerate… For a PoC that will be destroyed this may be fine… but do not use this expression as is long-term solution or in production …
Arrays.isEmpty(Arrays.toCsvString(Groups.startsWith("active_directory","",50))) ? Groups.startsWith("OKTA","",50) : Arrays.flatten(Groups.startsWith("OKTA","",50),Groups.startsWith("active_directory","",50))
This essentially, set up your Okta authorization server to provide specific scopes for your application and specific claims. One of which being group membership which the VAULT application can use for multi-tenancy and authorization.
Vault Authorization Server–> Access Policies
You want something similar to below, however you should disable implicit flow as VAULT won’t be using it…. configure your tokens TTLs per your needs and risk…. The lower the better in our case.

Vault Authorization Server–> Token Review

The token preview feature ^^^above^^^ can be used to test whether your authorization server is creating a token with the necessary scopes and claims that the VAULT application needs. In this case, it looks like we’re getting the correct attributes inside the ID token during the authorization code flow. Most importantly the “groups” claim…..
VAULT will be configured as follows….
user_claim = "email"
role_type = "oidc"
bound_audiences = var.okta_bound_audiences
oidc_scopes = [
"openid",
"profile",
"email",
"groups",
]
groups_claim = "groups"
}
OKTA – configure the vault oidc application
Now we’ll need to configure the VAULT OIDC application settings within Okta to use the authorization server we just enabled in the previous steps…
Okta — > Applications –> Create App Integration

You can optionally configure refresh and implicit however VAULT should not use these settings based on what I’m seeing in the source-code. You can validate vaults OIDC client behavior yourself in their source code below..
https://github.com/hashicorp/cap
https://github.com/hashicorp/vault-plugin-auth-jwt
The imported library used in the Vault’s jwt auth method in oidc.go package supports both

The oidc auth method module invoking auth code flow

Back to Okta itself ….. VAULT supports both a Web App auth flow and a CLI based auth flow, callback URI’s are required to support both …
These are the urls that Okta will redirect back to with the authorization code … These values must also explicitly map to the “allow” list configured during the VAULT set-up … more on that later during the terraform example….

Applications –> Sign-On

WARNING: I’m using that same claim expression to find my active directory groups, however that expression must not be used without properly editing it with a Groups.contains(“your-vault-ad-group”) type of filter to avoid information disclosure ….
Arrays.isEmpty(Arrays.toCsvString(Groups.startsWith("active_directory","",50))) ? Groups.startsWith("OKTA","",50) : Arrays.flatten(Groups.startsWith("OKTA","",50),Groups.startsWith("active_directory","",50))
Applications –> Assignments

Applications –> Assignments
You can further reduce permissions here and remove domains.read from the grant. Instead only use groups.read and users.read


You should now have everything you need on the Okta side to supports the OIDC auth flow, authenticate against your active directory and provide proper scopes and claims back to VAULT to test basic VAULT functionality…….

VAULT – OIDC – Authentication Method

VAULT supports both a JWT flow and OIDC flow authentication flow. I’d suggest you read general documentation below. Per Hashicorp documentation, VAULT supports a standard authorization code flow with PKCE.
You can also reverse engineer most of their requirements and configuration options from their API documentation… all of thee documents are severely lacking in the end-to-end understanding of the integration however …
- https://learn.hashicorp.com/tutorials/vault/oidc-auth?in=vault/auth-methods
- https://www.vaultproject.io/docs/auth/jwt
- https://www.vaultproject.io/api/auth/jwt
VAULT PREREQS
Before we begin, I’m assuming you already have a KV store enabled and some secrets within it, if not you’ll have to go do that yourself following these steps…
https://learn.hashicorp.com/tutorials/vault/getting-started-intro?in=vault/getting-started
OIDC STEPS
First, I created a simple policy called “manager” which only has access to the KV secrets, however this policy cannot “Admin” the vault server …. this policy will be mapped to the OIDC Auth method to avoid exposing admin services during dev-test.
path "/kv/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
VAULT – OIDC CONFIGURATION – AS CODE

The terraform source code used to configure the baseline VAULT settings can be found here… essentially this avoids writing a bunch of CLI commands and writing your own custom CURL commands
https://github.com/secSandman/vault-oidc-setup/raw/main/oidc-tf-cleaned.zip
Secondly, we run the terraform code
set VAULT_ADDR= <VAULT ADDRESS>
set VAULT_TOKEN – <YOUR TOKEN>
Unzip –> oidc-tf.zip –> cd into the folder
terraform init
Let’s take a look at the source code to reverse engineer the basic parameters needed to configure the OIDC Authentication Method …
oidc-tf.zip –> modules–> oidc –> main.tf
resource "vault_jwt_auth_backend" "okta_oidc" {
description = "Okta OIDC"
path = var.okta_mount_path
type = "oidc"
oidc_discovery_url = var.okta_discovery_url
bound_issuer = var.okta_discovery_url
oidc_client_id = var.okta_client_id
oidc_client_secret = var.okta_client_secret
tune {
listing_visibility = "unauth"
default_lease_ttl = var.okta_default_lease_ttl
max_lease_ttl = var.okta_max_lease_ttl
token_type = var.okta_token_type
}
}
resource "vault_jwt_auth_backend_role" "okta_role" {
for_each = var.roles
backend = vault_jwt_auth_backend.okta_oidc.path
role_name = each.key
token_policies = each.value.token_policies
allowed_redirect_uris = [
"${var.vault_addr}/ui/vault/auth/${vault_jwt_auth_backend.okta_oidc.path}/oidc/callback",
# This is for logging in with the CLI if you want.
"http://localhost:${var.cli_port}/oidc/callback",
]
user_claim = "email"
role_type = "oidc"
bound_audiences = var.okta_bound_audiences
oidc_scopes = [
"openid",
"profile",
"email",
"groups",
]
groups_claim = "groups"
}
You’ll notice that a few of these values directly coincide with the values we configured in our Okta configurations. Take special note of the following, because these parameters must “match up” between both the VAULT application as the relying party and and Okta as the OpenID Provider (OP).
- Web App redirect URI for Okta callbacks
- CLI redirect URI for Okta callbacks
- oidc discovery url
- oidc_scopes
- user_claim
- bound_audience
Most of the above parameter can be found by navigating below …or on the next tab
- Okta –> Security –> Vault Authorization server –> General
Now, lets look at the root terraform main function …
oidc-tf.zip –> main.tf
WARNING: The default VAULT token TTL in the TF code WAY too high, change it in the /modules/oidc/variable.tf or else risk long-live tokens being exfiltrated and replayed against your VAULT SERVER!!!
module "okta" {
source = "./modules/oidc"
okta_discovery_url = "<Discovery URL>"
okta_client_id = "<client_id>"
okta_client_secret = var.okta_secret
vault_addr = "<Vault dns address>"
okta_bound_audiences = [
"api://vault",
"<client_id>"
]
roles = {
manager = {
token_policies = []
}
}
}
terraform apply

At this point, I haven’t added any more advanced features such as bound claims, bound ip address etc. etc. I’m just testing basic authentication without anything fancy. However, if you want to know what other features the OIDC method and the OIDC role support you can red below…
- https://www.vaultproject.io/api/auth/jwt
- https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/identity_oidc_role
Navigate to your VAULT URL

I’m going to enter the same AD user that I used earlier during the delegated AD authentication test in Okta. This is the same user that is added to my vault-oidc-1 group membership. However, I am not using the group_claims at this point yet for authorization.

secandman user is now logged in as a domain user in the VAULT application. However,this user cannot do much of anything ….


VAULT – OIDC – Authorization
We now need to handle the authorization for our AD user. A couple quick notes that helped me in the journey.
Once a user authenticates to VAULT, an “entity” is created of that user with a corresponding alias. The user entity alias is essentially the user+authentication method combination. This is because VAULT supports many authentication methods in parallel, all of which, could have the same use principal. The application tries to reconcile this with a one-to-many relationship. You can use the unique mapping to apply policies to entities such as IF User XYZ logs in from ABC then apply POLICY XYZ_ABC ELSE …
https://learn.hashicorp.com/tutorials/vault/identity
Similarly, VAULT allows you to simplify user authorization management using “Groups” instead of direct policies applied at a user level.
VAULT supports INTERNAL and EXTERNAL groups… we want EXTERNAL groups because that refers directly to external authentication providers
“In order to manage the group-level authorization, you can create an external group to link Vault with the external identity provider (auth provider) and attach appropriate policies to the group.”

We now need to map the external group somehow to he group_claims that we’re passing inside of the JWT. Welcome Group Alias .…
“By default, Vault creates an internal group. When you create an internal group, you specify the group members rather than group alias. Group aliases are mapping between Vault and external identity providers (e.g. LDAP, GitHub, etc.). Therefore you define group aliases only when you create external groups. For internal groups, you specify member_entity_ids
and/or member_group_ids
.”

The group alias name should directly map to the AD group being passed inside of the claims in the token from the authorization server. VAULT will parse the groups claim key value pair and attempt to map it to this alias. If there is a match it will apply the Vault Group policy, in this case, “manager” policy with access to manager the VAULT KV store.
Finally, attempt to log again using an incognito window… and you’ll see the secsandman@securitysandman.com is not only authenticated but also authorized to manage the KV secrets store in VAULT.
Negative Test: If you remove the alias and repeat these steps, then you’ll find the user is no longer authorized.

OKTA – MFA & BEHAVIOR DETECTION
At this point, we have a domain joined user successfully authenticated and authorized to our secrets management solution via OIDC Auth Method + Okta. However, I’ve just shared the user name with the world wide web and some tricksters might think they can brute force and guess the password just for laughs.
Let’s enable MFA using Okta verify number challenge to further protect this login flow and avoid confusion based click-through attacks… notice I disabled PUSH notifications because they are often subject to social engineering attacks…





VAULT is protected with an OIDC + Okta Verify number challenge and local authorization.
FINAL ARCHITECTURE

Let’s talk about behavior detections a nice added bonus… Okta behavior detection enables you to configure sign-on policy rules that take into account changes in user behavior. For example, you can configure a policy to require multifactor authentication if a user signs in from a new location or using a new device.

VAULT – OPEN IAM SECURITY ISSUES
LOCAL STORAGE OF ACCESS TOKENS

Vault has had an open security issue filed since 2019 regarding storage of their tokens in localStorage on the browser. This is an inherent risk across all the authentication methods in VAULT and not specific to OIDC flows.
https://github.com/hashicorp/vault/issues/7476
Although some can and do argue about the combination of single page application (SPA’s), CORS, short lived tokens, private networks and removing query string parameters as an attacker vector to lower the risk of XSS an exfiltration of tokens, the fact remains …. if you can get a XSS to fire then….

+ TOKENS DO NOT REVOKE UPON SIGN OUT
Since, 2017 VAULT ha allowed tokens to persist and remain active even when a user logs out. During the OIDC configuration, you have the option to reduce the TTL on these tokens to limit their exposure. VAULT has added a revoke command in the UI which must be performed in addition to “sign-out”. This is an inherent risks for all authentication methods, not just the OIDC flow.
https://github.com/djenriquez/vault-ui/issues/222

but we end with JWT …..


