Login to Epicor Kinetic with ANY OpenID Connect Provider (On-Prem) Okta, Auth0, Entra, Facebook🤪: (Ok maybe not facebook)

And… Story Time… :popcorn:

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… :thinking::thought_balloon:

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

IDP

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 :winking_face_with_tongue:.

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 :exploding_head:.

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.

  1. Issuer must match EXACTLY. The token’s iss must 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).
  2. Audience must ne epicor_erp. and it doesn’t allow any other audience… so a single extra audience (looking at you, Keycloak’s default account) fails the whole token. Use epicor_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 :expressionless_face:).


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 becomes https://idp.yourcompany.com/realms/epicor.
    Realm names are case-sensitive (sometimes). Confirm the canonical value at …/.well-known/openid-configuration in the "issuer" field. Whatever case that is (mine was lowercase epicor), you must use it verbatim later. I burned time entering Epicor while the issuer was epicor because 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 :face_with_symbols_on_mouth::unamused_face::face_with_steam_from_nose::enraged_face::face_with_symbols_on_mouth::angry_face_with_horns:)
  • 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://notused and https://notused/* (you may need others for EMWW)
    • Epicor’s desktop client uses the literal placeholder redirect https://notused and 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 a 400 Invalid Request with no client_id in the URL.

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 typed epicor_erp (with a space) and the exact-match audience check failed until I caught it.
  • Assign epicor_erp to 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 stops account from polluting aud.

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? :eyes::crying_cat:)

2.4 Content Security Policy (The BANE OF EVERY web developers existence)

Insert a couple of hours of cursing here :face_with_symbols_on_mouth::face_with_symbols_on_mouth::face_with_symbols_on_mouth::face_with_symbols_on_mouth:

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/.configuration and confirm:
    • "IdentityProvider": { "Enabled": true, "Endpoint": "…", "ApiScope": "epicor_erp", … }
    • "TokenAuthentication": { "AzureBindingEnabled": false }. If this is still true, IdP won’t show.


Part 3: Troubleshooting things as you go, crap I learned and the source of most of the cursing.

  1. 400 Invalid Request, no client_id in the URL. Native Application ID was blank, and/or https://notused wasn’t registered. Set it, recycle, restart the desktop client.
  2. invalid_scope. The IDP didn’t recognize current_tenant. Create that scope.
  3. CSP ā€œviolates connect-srcā€ plus Cannot read properties of null (reading 'some'). IDP host not allow-listed in the Home web.config CSP. The null error is just the blocked-discovery fallout.
  4. 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 sent https://myserver…. Add the lowercase origin.
  5. IDX10231: Audience validation failed. The dreaded account audience. Remove the ā€œAudience Resolveā€ mapper from the roles scope. Remember: making roles optional won’t help, because Kinetic explicitly requests it.
  6. Issuer mismatch / discovery 404. Case-sensitive realm and issuer. Match the discovery doc’s issuer value exactly.
  7. ā€œ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 :winking_face_with_tongue: 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)

NIce!!! as we start our CMMC journey this will come in handy..

TY Jose!!!

This is awesome! We run on Okta so we’ll see how it goes with that.

Michael Jackson Popcorn GIF

I’d love for you to try it, I know Okta is huge in Enterprise :crossed_fingers:

What a project, love to see the passion!

Confused Thinking GIF by Derek Tee

It’s too early for this. :sweat_smile:

You know the feeling though, you just crushed a personal project, you want to let the masses know! haha, this stuff gets me going on a Monday morning.

So who is gonna try it on cloud first? @klincecum or @GabeFranco ? :laughing:

Seems like the Content Security Policy edit would be greatest road block. Even if it was possible to edit it, seems like it could change back on an update..

Sounds easy!

It really is, the story is long and wordy cause I am a super nerd. But the gist is, make 3 clients one for web, one native one for server user exchange.
Configure a feature flag in Epicor
Plug in the 3 client names and secrets and #OffToTheRaces

10 minute setup

And I even understood most of the words in the story… it is a brilliant combination of:

  1. Problem identification (nobody seems to pay as much attention to this as they shoud)
  2. Solution description (also all too often skipped over)
  3. Investigation of problems encountered
  4. overcoming problems encountered
  5. documenting steps for each problems’ solution
  6. (repeat steps 3-5 an infinite number of times)
  7. string the entire solution together
  8. test ruthlessly (poor ruth!)
  9. tell everyone how easy it is now.

My dad was a Field Inspector for Underwriters Laboratories (back in the day). In the late 70s he went to all the Raytheon and Avco plants outside of Boston (NOT the NASA side, but he talked to a lot of those folks). They were just getting into the manufacture of integrated circuits… and one of the engineers told him something like, yeah, building that first chip costs five million dollars… but the second one and all the rest are like a nickel apiece.

ALL of us understand and appreciate the 5 million dollar (in late 70s money) weekend you just invested so we can do it all for a nickel.

I feel like any issues stemming from it would immediately be disregarded by support, so it’d be a risk to try it.

Honestly, from how easy @josecgomez made it look, I don’t understand why they don’t have more out of the box presets you can select from, or at the very least a UI we can use to set it up.

nerds GIF

4 Likes

Are we sure Epicor uses Implicit flow anymore? Especially in a browser? The OAuth 2.0 best security practices recommends against Implicit Grant Flow. I don’t see the requirement for it in the current Kinetic Installation Guides either. :person_shrugging:

More questions later…but this is VERY cool! Nice job!

It does in 2025.2 at last that’s what I observed in the exchange. It specifically requests response_type=id_token token

And I don’t have an option to tell it otherwise. I can only go off the observed exchange though so maybe if the client supports AuthorizationCode + PKCE it would use it but this specific request initiated by the Kinetic Web App calls for id_token token :man_shrugging:

@Olga do you know?

homepage still uses it AFAIK :woman_shrugging: I remember I switched IDP portal itself to PKCE last time I worked there, several years ago.

When you create an app in the IDP portal, there is legacy JS template - for implicit, and another one for PKCE.

Unfortunately Kinetic UX implementation was done before the PKCE became a standard and they did not rework it since then.

I’m guessing suport burden…

Again being the giant Auth nerd that I am, I know and fully understand how frigging painful it is to troubleshoot this stuff. I can image the calls to support now about ā€œIts not workingā€ and it just being the fact that you spelled ePicor in one url (not that I know ANYONE who would do that :expressionless_face:) but it takes support 4 weeks to figure that out.

Or I supose if one were thinking about it maliciously (read revenue) it is a nice SaaS only hook :man_shrugging:

Do you have any starter resources on Auth methods? Like blogs, or youtube series or repos or anything that you used along the way to study it/learn/implement it in your own projects?