And⦠Story Time⦠![]()
Years ago I had a conversation with @Bart_Elia about the architecture behind Epicor IDP. He mentioned that when they built it, they chose an open-source product called IdentityServer as the foundation. That detail sat in the back of my head for a long time.
This weekend a light bulb finally went off while trying to solve an issue related to CMMC2 cert. CMMC2 requires 2nd factor, and the only way to get that with Epicor is Entra or Epicor IDP. Problem is I donāt have Entra in my CMMC2 plant for.. scope reasons, and I canāt get Epicor IDP on prem⦠or can I⦠![]()
![]()
IdentityServer isnāt magic. Itās a standard OpenID Connect (OIDC) identity provider, the same protocol that Okta, Auth0, Microsoft Entra, and Keycloak all speak. So if Epicor IDP is ājustā a branded OIDC server, then the Kinetic client and app server must be talking plain OIDC under the hood, thereās no way that Epicor up and decided to recreate authentication from scratch. And that means there is no real technical reason you couldnāt point Kinetic at ANY OpenID Connect compatible IDP as long as Epicor didnāt hard code a bunch of stuff.
The login screen only advertises three options: Microsoft Entra (Azure AD), Basic Auth, and āEpicor IDP.ā (on SaaS).But if Iām right that 3rd option doesnt have to be just SaaS and it doesnāt have to be limited to Epicorās branded IDP, any Open ID Compatible provier should work (unless Epicor did somethig really dumb like hard code secrets) So I spent a weekend testing the theory against one of my on-prem Kinetic environments. We already use both Entra and KeyCloak as IDP providers so I had options, but first I had to understand Epicorās implementation. One of the beautiful things about Open ID is that the authentication happens in the client in front of your eyes, you can see everything coming and going.
Spoiler: it works. Hereās everything, including the exact files you touch and the gotchas I ran into

The first thing I had to do was figure out if Epicor was indeed using a standard OIDC client, so I asked a friend who uses IDP to let me watch them login. What I observed was a standard OIDC flow, Auth, Token, User Info and Exchange. A couple āpeculiarā scopes but everything looked prett standard. As @Banderson can attest to via making fun of me for being a āgiant nerdā I spent way way way too much time a few years ago implementing a custom Authentication protocol called SQRL and my āclaim to fameā at the time was writng an OAuth implementation of the SQRL protocol which allowed SQRL to be compatible with any standard OAuth supporting client / server (and this was before Ai so be impressed!!! damn it). Long story⦠long, I have been elbows deep in the OAuth and OpenID RFCS so I know enough to be able to extrapolate information just gleaned from the exchange plus some of the āstandardsā around it. So I will call this one a win, and #proof that being a Giant Nerd about Auth is sometimes useful
.
It seems that the client is a generic OIDC Client. It fetches a config from the app server (/api/.configuration) and builds a normal OIDC authority, client_id, and scope from it. All this was plainly visible if you just watch the exchange, I frigging LOVE crypto, you can somehow watch it all happen in front of your eyes and yet it is fully cryptographicaly secure
.
It fetches your IDPās /.well-known/openid-configuration and JWKS, checks the RS256 signature, the issuer, the audience, and expiry. No proprietary trust, no Epicor-only crypto (as far as I can tell).
The validation has two strict, opinionated rules baked into the code that you must satisfy, and these are where the weekend went. There was a hell of a lot of trial and error.
- Issuer must match EXACTLY. The tokenās
issmust equal the āIdentity Provider URLā you configure, character for character (yes, case-sensitive as well.. this last piece almost killed me after 45 minutes of /ERP vs /erp⦠FML). - Audience must ne
epicor_erp. and it doesnāt allow any other audience⦠so a single extra audience (looking at you, Keycloakās defaultaccount) fails the whole token. Useepicor_erp.
Keep those two in your head. Everything below exists to make them true.
Epicor treats this as a SaaS only feature:
According to every shread of info I could find, the instructions for this simply do not exist as a customer facing document that I could find so I had to reverse engineer the setup slowly one piece at a time.
The Identity Provider configuration is gated behind a feature flag, I found a Db table I could populate but not UI, it took me forver to remember the damn feature flags. On a fresh on-prem install you wonāt see the āConfigure Identity Provider Authenticationā option (UI) in admin console unless you enable the feature flag.
Everything works wihtout it, except you get a nice UI to do the setup if you enable it first (I sadly learned this AFTER i had finished the setup by hand in the Db
).
Part 1: Configure your OpenID Connect provider (Keycloak example,but this works for any OpenID capable Identity Provider with same instuctions / concepts)
Youāll create three clients and a couple of scopes. Epicorās smart/desktop client and the browser client use different OAuth flows, hence multiple clients.
1.1 Realm / Authority
- Keycloak: create a realm, e.g.
epicor. Your issuer becomeshttps://idp.yourcompany.com/realms/epicor.
Realm names are case-sensitive (sometimes). Confirm the canonical value atā¦/.well-known/openid-configurationin the"issuer"field. Whatever case that is (mine was lowercaseepicor), you must use it verbatim later. I burned time enteringEpicorwhile the issuer wasepicorbecause Iām a moron⦠and also because someone decided that casing somehow applies to URLs that should just be ilegal.
1.2 Browser (āWebā) client, implicit flow
This is what the Kinetic web app uses. Think the new Kinetic UX Authentication flow.
- Client ID:
epicor-kinetic-web - Public client (no secret, āclient authentication offā)
- Enable the Implicit flow (Kinetic requests
response_type=id_token token) - Valid redirect URIs: your app base, e.g.
https://YOUR-APPSERVER/EpicorX_SSL/Apps/Erp/Home/*(Case sentisive




) - Web origins (CORS):
https://your-appserver, lowercase (can you spot the pattern)
1.3 Native (ādesktopā) client, authorization code + PKCE (Think Epicor Classic)
- Client ID:
epicor-kinetic-native(These are IDās I chose, they can be whatever you want) - Public client (uses PKCE, no secret)
- Enable the Standard flow (authorization code)
- Valid redirect URIs:
https://notusedandhttps://notused/*(you may need others for EMWW)- Epicorās desktop client uses the literal placeholder redirect
https://notusedand intercepts it internally. It doesnāt need to be real, just registered. If itās missing (or the Native App ID is blank), you get a400 Invalid Requestwith noclient_idin the URL.
- Epicorās desktop client uses the literal placeholder redirect
1.4 Server client, client credentials
The app server makes back-channel calls to the IDP to get user info (Email) it needs its own client
- Client ID:
epicor-kinetic-server - Confidential client (client authentication on), Service Accounts / client_credentials enabled
- Grab the client secret (youāll paste it into Epicor, which stores it encrypted)
1.5 The epicor_erp scope plus Audience mapper
The browser requests a scope, and that scope must stamp aud = epicor_erp.
For KeyCloak:
- Create a client scope named
epicor_erp. - Add a protocol mapper of type Audience:
- Included Custom Audience =
epicor_erp(use the custom field, not āclient audienceā) - Add to access token = ON
Pay attention, do not leave a trailing space. I literally typedepicor_erp(with a space) and the exact-match audience check failed until I caught it.
- Included Custom Audience =
- Assign
epicor_erpto all three clients.
1.6 The current_tenant scope
The Kinetic client also requests a current_tenant scope. Remeber IDP is on SaaS mostly so it assumes a tenantId is passed in with every request so you need the scope even if its blank for on prem.
- Create an empty client scope named
current_tenant(no mappers needed) and assign it to the clients.
1.7 Kill the stray account audience (on KeyCloak)
Even after the above, Keycloak added account to aud because the roles scopeās āAudience Resolveā mapper resolves the userās account client-roles into an audience. And making roles optional did NOT help, because the Kinetic client explicitly requests roles (I confirmed this in the raw authorize URL: scope=openid email roles epicor_erp).
- On KeyCloak: Go to Client scopes,
roles, Mappers, then delete (or turn off āadd to access tokenā on) the āAudience Resolveā mapper. This keeps realm roles available but stopsaccountfrom pollutingaud.
1.8 Claims and signing (mostly defaults)
-
The token must carry the userās
email, and it must match an Epicor user record (Epicor maps identity by email, External Identity Field). This is default behavior for 99% of IDPs so I wouldnāt worry about it too much. -
Signing must be RS256 (Keycloak, Okta, Auth0 default) so the server can validate via your JWKS.
Part 2: Configure Epicor (the server side)
2.1 Turn on the Identity Provider feature flag
As I said before I found this later there are is a harder way to do this by updating a Db Table but honeslty this makes it work out of the box with a UI and all you need to do is enable one feature flag via REST.
CURL POST TO:
https://<TLD>/<EpicorInstance>api/v2/odata/C001/Ice.Lib.FeatureFlagSvc/EnableFeature
{
"featureID": "8FA3AF1A-89B8-4978-8812-7245A02ACFF2",
"level": 0,
"target": ""
}
(level 0 = System, the whole system. featureID is the built-in āEnable support for Identity Providerā flag.) Once itās on, the āConfigure Identity Provider Authenticationā action shows up in the Administration Console on your on-prem app server.
2.2 Fill in āConfigure Identity Provider Authenticationā
Epicor Administration Console, your app server, Actions, Configure Identity Provider Authentication. These fields write to the Ice.IdentityProvider table, and the secret is encrypted for you.
| Epicor field | Value (Keycloak example) |
|---|---|
| Enable Epicor Identity Provider | checked |
| Epicor Identity Provider URL | https://idp.yourcompany.com/realms/epicor (exact iss, case matters) |
| Web Application ID | epicor-kinetic-web |
| Native Application ID | epicor-kinetic-native |
| Server Application ID | epicor-kinetic-server |
| Server Application Secret | (secret from 1.4) |
| Token Scope for Clients | epicor_erp |
2.3 Disable Azure binding (if you have it)
This one surprised me. Azure AD and IdP are mutually exclusive as the primary login on a given app server. The Kinetic client picks ONE token method via an exclusive if (Azure) ⦠else if (Windows) ⦠else if (IdP) chain, and Azure wins. So while Azure binding is on, IdP never appears in the dropdown.
The nice thing is that most / any IDPs including Entra itself can be configured to work with OIDP so if you go this route you can still use Entra.
Administration Console, App Server Configuration, uncheck Azure AD.) Basic Auth stays available as a fallback, so you donāt lock yourself out. (No I didnāt do this why do you ask? ![]()
)
2.4 Content Security Policy (The BANE OF EVERY web developers existence)
Insert a couple of hours of cursing here ![]()
![]()
![]()
![]()
Afer all that setup above everything worked except when I went to login on the web ⦠nothing happened. Turns out that the Kinetic Home app ships a strict CSP that allow-lists Epicor and Microsoft hosts but NOT your IDP, so the browser blocks the OIDC discovery, JWKS, and userinfo calls.
Edit the Home appās web.config (e.g. Drive:\YourServerFilePath\Server\Apps\Erp\Home\web.config), find the Content-Security-Policy header, and add your IDP host to connect-src and form-action (the frame-src * wildcard already covers the silent-renew iframe):
connect-src 'self' ⦠https://login.microsoftonline.com https://idp.yourcompany.com;
form-action 'self' ⦠https://idp.yourcompany.com
2.5 Recycle and verify
- Recycle the IIS Application Pool (the IDP settings are cached at startup).
- Browse
https://YOUR-APPSERVER/EpicorInstance/api/.configurationand confirm:"IdentityProvider": { "Enabled": true, "Endpoint": "ā¦", "ApiScope": "epicor_erp", ⦠}"TokenAuthentication": { "AzureBindingEnabled": false }. If this is stilltrue, IdP wonāt show.
Part 3: Troubleshooting things as you go, crap I learned and the source of most of the cursing.
400 Invalid Request, noclient_idin the URL. Native Application ID was blank, and/orhttps://notusedwasnāt registered. Set it, recycle, restart the desktop client.invalid_scope. The IDP didnāt recognizecurrent_tenant. Create that scope.- CSP āviolates connect-srcā plus
Cannot read properties of null (reading 'some'). IDP host not allow-listed in the Homeweb.configCSP. The null error is just the blocked-discovery fallout. - CORS: āNo āAccess-Control-Allow-Originā on /userinfoā. Your IDP only returns CORS headers for registered Web Origins, and the browser sends the origin lowercase. Iād registered
https://MYServerā¦(mixed case) but the browser senthttps://myserverā¦. Add the lowercase origin. IDX10231: Audience validation failed. The dreadedaccountaudience. Remove the āAudience Resolveā mapper from therolesscope. Remember: makingrolesoptional wonāt help, because Kinetic explicitly requests it.- Issuer mismatch / discovery 404. Case-sensitive realm and issuer. Match the discovery docās
issuervalue exactly. - āI fixed it but it still failsā. Token caching. After any IDP change, do a fresh login (clear the IDP SSO session or use incognito). The rejected token was minted before your fix.
The golden token. When a freshly issued access token decodes to this, youāre done, the Audience MUST be exactly that.
{
"iss": "https://idp.yourcompany.com/realms/epicor",
"aud": "epicor_erp",
"email": "you@yourcompany.com"
}
(signed RS256)
The cross-provider cheat sheet (this is Aiās take I donāt have access to Okta or Auth0 or Other Workspace)
| Concept | Keycloak | Okta | Auth0 | Generic OIDC |
|---|---|---|---|---|
| Issuer / Authority | Realm URL ā¦/realms/epicor |
Authorization Server issuer | Tenant domain | issuer from discovery |
| Browser client | Public client, Implicit flow | SPA app | SPA app | Public client, id_token token |
| Desktop client | Public client, code+PKCE, redirect https://notused |
Native app | Native app | Public, code+PKCE |
| Server client | Confidential, service accounts | API services (client creds) | Machine-to-Machine | Confidential client |
| Required audience | Audience mapper to epicor_erp |
Auth Server audience epicor_erp |
API Identifier epicor_erp |
aud = epicor_erp |
| API scope | epicor_erp client scope |
epicor_erp scope |
scope/audience | epicor_erp |
| Extra scope | current_tenant |
current_tenant |
current_tenant |
current_tenant |
| Kill extra audience | Remove roles āAudience Resolveā |
Trim default aud |
Drop default API aud | aud must be ONLY epicor_erp |
| CORS | Web Origins (lowercase) | Trusted Origins | Allowed Web Origins | CORS allow your app origin |
So Yes. āEpicor IDPā is, at its core, a standard OpenID Connect provider (built on IdentityServer), and the Kinetic client and server speak vanilla OIDC. There is nothing stopping you from putting any OpenID Connect compatible IDP (Keycloak, Okta, Auth0, Entra via brokering, you name it) in front of an on-prem Kinetic install.
The capability is fenced off as SaaS-only and hidden behind a feature flag, Iām guessing becasue it is hard to walk average joe through this setup, but the engine underneath is the same in every install. Flip the flag, satisfy Epicorās two opinionated validation rules (exact issuer match and a hardcoded audience of epicor_erp), and Kinetic doesnāt know or care whoās actually issuing the tokens.
@Bart_Elia, if youāre reading this, lo these many years later you saved my butt with this little side conversation 5+ years ago
on this CMMC2 crap, now we can happily satisfy CMMC2 2nd Factor requirements wihtout having to bring all of AZURE into scope.
Disclaimer: this is on-prem experimentation on a dev box, not an Epicor-supported configuration. Test thoroughly, keep Basic Auth as a fallback, and donāt do this blind on production. Donāt call support if it doesnāt work, they wonāt be able to fix it for you.
I am happy to try to help if someone has an IDP theyād like to try this on to put together more Provider Specific Guides (like I did for KeyCloak)











