diff --git a/changelog/17071.txt b/changelog/17071.txt
new file mode 100644
index 000000000..926ca839a
--- /dev/null
+++ b/changelog/17071.txt
@@ -0,0 +1,2 @@
+```release-note:feature
+**UI OIDC Provider Config**: Adds configuration of Vault as an OIDC identity provider, and offer Vault’s various authentication methods and source of identity to any client applications.
\ No newline at end of file
diff --git a/ui/app/adapters/named-path.js b/ui/app/adapters/named-path.js
new file mode 100644
index 000000000..383fa75b5
--- /dev/null
+++ b/ui/app/adapters/named-path.js
@@ -0,0 +1,76 @@
+/**
+ * base adapter for resources that are saved to a path whose unique identifier is name
+ * save requests are made to the same endpoint and the resource is either created if not found or updated
+ * */
+import ApplicationAdapter from './application';
+import { assert } from '@ember/debug';
+export default class NamedPathAdapter extends ApplicationAdapter {
+ namespace = 'v1';
+ saveMethod = 'POST'; // override when extending if PUT is used rather than POST
+
+ _saveRecord(store, { modelName }, snapshot) {
+ // since the response is empty return the serialized data rather than nothing
+ const data = store.serializerFor(modelName).serialize(snapshot);
+ return this.ajax(this.urlForUpdateRecord(snapshot.attr('name'), modelName, snapshot), this.saveMethod, {
+ data,
+ }).then(() => data);
+ }
+
+ // create does not return response similar to PUT request
+ createRecord() {
+ let [store, { modelName }, snapshot] = arguments;
+ let name = snapshot.attr('name');
+ // throw error if user attempts to create a record with same name, otherwise POST request silently overrides (updates) the existing model
+ if (store.hasRecordForId(modelName, name)) {
+ throw new Error(`A record already exists with the name: ${name}`);
+ } else {
+ return this._saveRecord(...arguments);
+ }
+ }
+
+ // update uses same endpoint and method as create
+ updateRecord() {
+ return this._saveRecord(...arguments);
+ }
+
+ // if backend does not return name in response Ember Data will throw an error for pushing a record with no id
+ // use the id (name) supplied to findRecord to set property on response data
+ findRecord(store, type, name) {
+ return super.findRecord(...arguments).then((resp) => {
+ if (!resp.data.name) {
+ resp.data.name = name;
+ }
+ return resp;
+ });
+ }
+
+ // GET request with list=true as query param
+ async query(store, type, query) {
+ const url = this.urlForQuery(query, type.modelName);
+ const { paramKey, filterFor, allowed_client_id } = query;
+ // * 'paramKey' is a string of the param name (model attr) we're filtering for, e.g. 'client_id'
+ // * 'filterFor' is an array of values to filter for (value type must match the attr type), e.g. array of ID strings
+ // * 'allowed_client_id' is a valid query param to the /provider endpoint
+ let queryParams = { list: true, ...(allowed_client_id && { allowed_client_id }) };
+ const response = await this.ajax(url, 'GET', { data: queryParams });
+
+ // filter LIST response only if key_info exists and query includes both 'paramKey' & 'filterFor'
+ if (filterFor) assert('filterFor must be an array', Array.isArray(filterFor));
+ if (response.data.key_info && filterFor && paramKey && !filterFor.includes('*')) {
+ const data = this.filterListResponse(paramKey, filterFor, response.data.key_info);
+ return { ...response, data };
+ }
+ return response;
+ }
+
+ filterListResponse(paramKey, matchValues, key_info) {
+ const keyInfoAsArray = Object.entries(key_info);
+ const filtered = keyInfoAsArray.filter((key) => {
+ const value = key[1]; // value is an object of model attributes
+ return matchValues.includes(value[paramKey]);
+ });
+ const filteredKeyInfo = Object.fromEntries(filtered);
+ const filteredKeys = Object.keys(filteredKeyInfo);
+ return { keys: filteredKeys, key_info: filteredKeyInfo };
+ }
+}
diff --git a/ui/app/adapters/oidc/assignment.js b/ui/app/adapters/oidc/assignment.js
new file mode 100644
index 000000000..0c78f6492
--- /dev/null
+++ b/ui/app/adapters/oidc/assignment.js
@@ -0,0 +1,7 @@
+import NamedPathAdapter from '../named-path';
+
+export default class OidcAssignmentAdapter extends NamedPathAdapter {
+ pathForType() {
+ return 'identity/oidc/assignment';
+ }
+}
diff --git a/ui/app/adapters/oidc/client.js b/ui/app/adapters/oidc/client.js
new file mode 100644
index 000000000..3331b6d7e
--- /dev/null
+++ b/ui/app/adapters/oidc/client.js
@@ -0,0 +1,7 @@
+import NamedPathAdapter from '../named-path';
+
+export default class OidcClientAdapter extends NamedPathAdapter {
+ pathForType() {
+ return 'identity/oidc/client';
+ }
+}
diff --git a/ui/app/adapters/oidc/key.js b/ui/app/adapters/oidc/key.js
new file mode 100644
index 000000000..0b7561ed0
--- /dev/null
+++ b/ui/app/adapters/oidc/key.js
@@ -0,0 +1,11 @@
+import NamedPathAdapter from '../named-path';
+
+export default class OidcKeyAdapter extends NamedPathAdapter {
+ pathForType() {
+ return 'identity/oidc/key';
+ }
+ rotate(name, verification_ttl) {
+ const data = verification_ttl ? { verification_ttl } : {};
+ return this.ajax(`${this.urlForUpdateRecord(name, 'oidc/key')}/rotate`, 'POST', { data });
+ }
+}
diff --git a/ui/app/adapters/oidc/provider.js b/ui/app/adapters/oidc/provider.js
new file mode 100644
index 000000000..064e56968
--- /dev/null
+++ b/ui/app/adapters/oidc/provider.js
@@ -0,0 +1,7 @@
+import NamedPathAdapter from '../named-path';
+
+export default class OidcProviderAdapter extends NamedPathAdapter {
+ pathForType() {
+ return 'identity/oidc/provider';
+ }
+}
diff --git a/ui/app/adapters/oidc/scope.js b/ui/app/adapters/oidc/scope.js
new file mode 100644
index 000000000..af69799d8
--- /dev/null
+++ b/ui/app/adapters/oidc/scope.js
@@ -0,0 +1,7 @@
+import NamedPathAdapter from '../named-path';
+
+export default class OidcScopeAdapter extends NamedPathAdapter {
+ pathForType() {
+ return 'identity/oidc/scope';
+ }
+}
diff --git a/ui/app/components/oidc/assignment-form.js b/ui/app/components/oidc/assignment-form.js
new file mode 100644
index 000000000..d521bce93
--- /dev/null
+++ b/ui/app/components/oidc/assignment-form.js
@@ -0,0 +1,72 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { tracked } from '@glimmer/tracking';
+
+/**
+ * @module Oidc::AssignmentForm
+ * Oidc::AssignmentForm components are used to display the create view for OIDC providers assignments.
+ *
+ * @example
+ * ```js
+ *
{{@attr.options.subText}}
{{#if @attr.options.docLink}}
-
+
{{attr.options.subText}}
{{#if attr.options.docLink}}
-
+
+ Example of a JSON template for scopes: +
+
+ The full list of template parameters can be found
+
+ Configure Vault to act as an OIDC identity provider, and offer
+ {{"Vault’s"}}
+ various authentication
+ {{#if this.isCta}}
+
+ {{/if}}
+ methods and source of identity to any client applications.
+
+ Step 1: + Create an application, and obtain the client ID, client secret and issuer URL. +
++ Step 2: + Set up a new auth method for Vault with the client application. +
+Use scope to define identity information about the authenticated user.
+
{{@subText}}
{{#if @docLink}}
-
+
{{@attr.options.subText}}
{{#if @attr.options.docLink}}
-
+
{{if
- (gte this.displayArray.length 10)
- (concat this.displayArray ", and " (dec 5 this.displayArray.length) " more.")
- this.displayArray
+ (gte @displayArray.length 10)
+ (concat @displayArray ", and " (dec 5 @displayArray.length) " more.")
+ @displayArray
}}
{{/if}}
diff --git a/ui/lib/core/addon/components/info-table-item-array.js b/ui/lib/core/addon/components/info-table-item-array.js
index 7d3dcad27..2c316de6e 100644
--- a/ui/lib/core/addon/components/info-table-item-array.js
+++ b/ui/lib/core/addon/components/info-table-item-array.js
@@ -15,25 +15,25 @@ import { isWildcardString } from 'vault/helpers/is-wildcard-string';
* @example
* ```js
*