REST API with Single Sign On

I know there are many posts about the REST API and SSO, but for some reason I am not understanding what I need to do. I am working in VBA. Here is my current code that works with Basic Auth (prompts the user for credentials)

        strUrl = "https://tpa1-eas01/EpicorERPPilot/api/v1/BaqSvc/DataList_Parent_MAX/?PartNumber='" & strPartNum & "'"

    With objRequest
        .Open "GET", strUrl, blnAsync
        .SetRequestHeader "Content-Type", "application/json"
        .Send
        'spin wheels whilst waiting for response
        While objRequest.readyState <> 4
            DoEvents
        Wend
        strResponse = .ResponseText
    End With

What do I do to make this code work with SSO? We have enabled the Windows Authentication in IIS but it is prompting us for credentials but they never are accepted and we end up in an endless loop with the credential window always popping up.

I am hoping for some simple code that works with the SSO.

I’d start with looking at the REST Technical Reference guide (page 85). Epicor provides information about using SSO with the API.
Also make sure your endpoints are in your trusted sites list. Or you’ll get an extra prompt from Windows.

1 Like

I have been reading it, and that has added to my confusion. It says for SSO you need to enable Windows Authentication in IIS (which we have done). We are running under 10.2.600 so we have the proper setting in the web.config file. According to the documentation “This user can now connect to REST services without entering credentials”. However, I still get prompted for credentials.

Where do the endpoints need to be listed as trusted sites? Each computer or on the server?

Each Epicor server would be added to your trusted sites.

We have added the server as a trusted site but I still get the logon prompt. Is there any other way to code the GET to let it know we are using single sign on? Is there a way to get a token without coding in a user’s credentials?

You don’t. You will not be able to use Oauth SSO in VBA without calling an external process.

You could do your auth in powershell and feed it back.

Could you call the token service with a Basic Auth account like so?

Not exaclty SSO but get’s rid of the login prompt.

Option Explicit

Public Const stBaseUrl As String = "https://xxx.epicorsaas.com/xxx/api/v2/odata/xxxxxx"
Public Const stAPIKey As String = "KWwNYuijBwbkc......................................................."
Public Const stCredentials As String = "UserName" & ":" & "Password"
Public stUrl As String
Public stTokenResponse As String
'Public ri As New Dictionary

Sub Button1_Click()
Dim stResponse As String
    stResponse = CallBAQ()
    Range("G4").Value = stResponse
End Sub

Function CallBAQ() As String
Dim http As New MSXML2.XMLHTTP60
Dim stUrl As String, stPostBody, stResponse As String, sSuccess As Boolean

    stUrl = stBaseUrl & "/BaqSvc/baq_PartPrice/Data?PartNum='PL0011000'&$filter=Calculated_PrimarySupplier eq true"

    Set http = CallEpicorAPI("GET", stUrl, "", True, "")

    If http.ReadyState = 4 And (http.Status = 200 Or http.Status = 202) Then
        stResponse = http.responseText
        sSuccess = True
    Else
         stResponse = "Error" & vbNewLine & "Ready state: " & http.ReadyState & _
         vbNewLine & "HTTP request status: " & http.Status & _
         vbNewLine & http.responseText
         sSuccess = False
         MsgBox stResponse
    End If
    
    'Log stAction, stURL, CStr(stPostBody), blnAuthorize, http
    
Exit_Function:
    CallBAQ = stResponse
    Set http = Nothing
End Function

Function CallEpicorAPI(stAction As String, stUrl As String, stPostBody As Variant, blnAuthorize As Boolean, Optional stContentType As String) As MSXML2.XMLHTTP60

    Dim http As New MSXML2.XMLHTTP60
    Dim stResponse As String, sSuccess As Boolean

    http.Open stAction, stUrl, False
    If IsMissing(stContentType) Or 0 = Len(stContentType) Then stContentType = "application/json"
    http.setRequestHeader "Content-Type", stContentType
    http.setRequestHeader "x-api-key", stAPIKey
    If blnAuthorize Then http.setRequestHeader "Authorization", "Bearer " & GetToken()
    http.Send stPostBody

    If http.ReadyState = 4 And (http.Status = 200 Or http.Status = 202) Then
        stResponse = http.responseText
    '    sSuccess = True
    'Else
    '     MsgBox "Error" & vbNewLine & "Ready state: " & http.ReadyState & _
    '     vbNewLine & "HTTP request status: " & http.Status & _
    '     vbNewLine & http.responseText
    '     sSuccess = False
    End If
    
    'Log stAction, stURL, CStr(stPostBody), blnAuthorize, http
    
Exit_Function:
    Set CallEpicorAPI = http
    Set http = Nothing
End Function

Function GetToken() As String
Dim http As New MSXML2.XMLHTTP60
Dim stTokenURL As String, stBase64Credentials As String, stPostBody As String, stResponse As String


    stTokenURL = stBaseUrl & "/Ice.Lib.TokenServiceSvc/GetAccessToken"
 
    stPostBody = "{""clientId"": ""00000000-0000-0000-0000-000000000000"",""clientSecret"": ""string"",""scope"": ""string""}"
    
    stBase64Credentials = EncodeBase64(stCredentials)

    http.Open "POST", stTokenURL, False
    http.setRequestHeader "Authorization", "Basic " & stBase64Credentials
    http.setRequestHeader "x-api-key", stAPIKey
    http.setRequestHeader "Content-Type", "application/json"
    http.Send stPostBody

    If http.ReadyState = 4 And http.Status = 200 Then
        stTokenResponse = http.responseText
        stResponse = ParseJson(stTokenResponse, "AccessToken", "TokenService")
    Else
         MsgBox "Error" & vbNewLine & "Ready state: " & http.ReadyState & _
         vbNewLine & "HTTP request status: " & http.Status & _
         vbNewLine & http.responseText
    End If

Exit_Function:
    GetToken = stResponse
    Set http = Nothing
End Function

Function ParseJson(json As String, stPropName As String, Optional stParseFrom As String = "") As String
    Dim arrStart As Long, arrEnd As Long
    Dim arrJson As String
    Dim key As String
    Dim startPos As Long, endPos As Long
    Dim result As String

    ' If stParseFrom is provided, find the array/object first
    If stParseFrom <> "" Then
        arrStart = InStr(1, json, """" & stParseFrom & """:[", vbTextCompare)
        If arrStart = 0 Then
            ParseJson = ""
            Exit Function
        End If
        arrStart = arrStart + Len(stParseFrom) + 3 ' move past "TokenService":[
        arrEnd = InStr(arrStart, json, "]")
        If arrEnd = 0 Then
            ParseJson = ""
            Exit Function
        End If
        arrJson = Mid(json, arrStart, arrEnd - arrStart)
    Else
        arrJson = json
    End If

    key = """" & stPropName & """" & ":"
    startPos = InStr(1, arrJson, key, vbTextCompare)
    If startPos = 0 Then
        ParseJson = ""
        Exit Function
    End If

    startPos = startPos + Len(key)
    ' Skip spaces and quotes
    Do While Mid(arrJson, startPos, 1) = " " Or Mid(arrJson, startPos, 1) = """"
        startPos = startPos + 1
    Loop

    endPos = InStr(startPos, arrJson, """")
    If endPos = 0 Then
        ParseJson = ""
        Exit Function
    End If

    result = Mid(arrJson, startPos, endPos - startPos)
    ParseJson = result
End Function

Function EncodeBase64(text As String) As String
    Dim arrData() As Byte
    arrData = StrConv(text, vbFromUnicode)
    Dim objXML As Object
    Dim objNode As Object
    Set objXML = CreateObject("MSXML2.DOMDocument")
    Set objNode = objXML.createElement("b64")
    objNode.DataType = "bin.base64"
    objNode.nodeTypedValue = arrData
    EncodeBase64 = Replace(objNode.text, vbLf, "")
    Set objNode = Nothing
    Set objXML = Nothing
End Function

Function CallMSGraphAPI(stAction As String, stUrl As String, stPostBody As Variant, blnAuthorize As Boolean, Optional stContentType As String) As MSXML2.XMLHTTP60

    Dim http As New MSXML2.XMLHTTP60
    Dim stResponse As String, sSuccess As Boolean

    http.Open stAction, stUrl, False
    If IsMissing(stContentType) Or 0 = Len(stContentType) Then stContentType = "application/json"
    http.setRequestHeader "Content-Type", stContentType
    If blnAuthorize Then http.setRequestHeader "Authorization", "Bearer " & GetMSToken()
    http.Send stPostBody

    If http.ReadyState = 4 And (http.Status = 200 Or http.Status = 202) Then
        stResponse = http.responseText
    '    sSuccess = True
    'Else
    '     MsgBox "Error" & vbNewLine & "Ready state: " & http.ReadyState & _
    '     vbNewLine & "HTTP request status: " & http.Status & _
    '     vbNewLine & http.responseText
    '     sSuccess = False
    End If
    
    'Log stAction, stURL, CStr(stPostBody), blnAuthorize, http
    
Exit_Function:
    Set CallMSGraphAPI = http
    Set http = Nothing
End Function

1 Like

I have been digging and digging and have not been able to find a way to get SSO to work with REST. I found this for tokens in the Epicor REST guide:

private static string ObtainToken()
 {
 HttpClient client = new HttpClient();
 // Add an Accept header for JSON format.
 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHea
derValue("application/json"));
 try
 {
 const string TokenCreateURL = "https://EpicorServer/ERP102400/T
okenResource.svc/";
 var request = new HttpRequestMessage()
 {
 RequestUri = new Uri(TokenCreateURL),
 Method = HttpMethod.Post,
 };
 //send user credential
 request.Headers.Add("username", "manager");
 request.Headers.Add("password", "manager"); 

The last 2 lines here require a username and password, which defeats the purpose of SSO. Even to get a token you need a username and password.

We are moving to SSO and I need to get this working. Does anyone know how to just pass the user information (in our case their email address is used for SSO) without a password? I don’t mind having to get a token and then sending the token for my queries but I need to somehow get past any method that uses the password.

I have no idea because I’m not on prem so Windows sso is not an option. However, afaik, iis with windows auth turned on also requires browser cooperation to handle token flow (who knows if that’ll be honored by excel or wherever you’re running vba) but browser-side settings typically go like:

• Add your site to the Intranet zone.
• Enable automatic logon in browser settings or via Group Policy:
• Computer Configuration > Administrative Templates > Windows Components > Internet Explorer > Internet Control Panel > Security Page > Intranet Zone

You could confirm this is setup and working by hitting an api endpoint in your browser, but again who knows if it’ll work elsewhere.

I’m not aware of an api token flow that doesn’t require user/password nore one that uses windows auth outside the browser ‘automatic logon’ magic.

I feel like I am getting closer on this but I need some advice. I have been able to get a SSO token from our server using OAuth 2 (client id, secret, etc). I am using the client information set up for our Epicor SSO client.
My question is now how do I apply that to get the REST API calls to work? I have tried running the call with the bearer token generated but that doesn’t work. I have tried to get a token using GetAccessToken with the generated token and that does not work either. No matter what I try I get the error: IDX10231: Audience validation failed.
I am working on all this is Postman just to figure out the proper sequence of calls.

After reading another article on this site Azure AD Authentication (REST via JavaScript / Angular) - Experts’ Corner - Epicor User Help Forum, I see that the scope should have /user_impersonation at the end but if I don’t put in .default I get an error: client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).

What should my scope really be? I also don’t understand the comment about changing the name of the scope in what appears to be the SSO setup. Should that have /user_impersonation on it?

All Epicor examples are for the flow with authentication, when user is supposed to login, either in smart client, or in web browser UI, not when client credentials are used. Only logged in used will have mapping claim like email or UPN assigned.
So I don’t think you are going correct direction.
Probably, you still can do it, though I did not try, if you specify email as permanent claim in the token…
As for scopes, maybe you can specify several?

You can try to create a token using Epicor library first and then compare its content with the one you create, to see how claims are different

1 Like

You need to give it the scope of user_impersonation Grab the ID from here

That needs to be passed in the the scopes array

Then you need to make a request to Azure to aquire a token using that scope (after you receive the auth token)

Then you’ll get back an accessToken which you can use as your Bearer token on subsequent epicor requests.

So depending on what you are doing it should go something like this note that the scope is an array…

  1. Standard Login call with Redirect. Use the came clientID as your Epicor application and the user_impersonation scope
GET https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?
    client_id={clientId}
    &response_type=code
    &redirect_uri={redirectUri}
    &scope={scopes}
    &state={state}
    &code_challenge={codeChallenge}
    &code_challenge_method=S256
  1. In the redirect you’ll get back the authorizatin code and you need to exchange it for a refresh token, again same clientId and scope, plus the authorization code you recieved. This may happen automatically depending on the library you are using. (MSAL)
POST https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={clientId}
&grant_type=authorization_code
&code={authorizationCode}
&redirect_uri={redirectUri}
&code_verifier={codeVerifier}
&scope={scopes}
  1. Then you Grab an Azure bearer token with your refresh token this should give you back an access_token which you can use directly with your Epicor call.
POST https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

client_id={clientId}
&grant_type=refresh_token
&refresh_token={refreshToken}
&scope={scopes}

Just tested it on Postman works great.

6 Likes

I am still confused on step 1. Where does the user information go? What goes in the code challenge? When I try to create a token as in the example nothing returns, just a blank page. If I leave it with no authorization I get some HTML returned that says sign in to your account. If I skip step 1 and go to step 2 and use the client secret instead of the code verifier I do get a token but that causes the audience error when I use the token. Step 1 is where I am missing what is really needed to send to get the code verifier.

Step 1 doens’t take any user information if you are using OAuth 2 Authorization Code. The First Request is an Authorize Request which re-directs the User to a Microsoft Page where they enter their username and password and “accept” the authentication.

This in turn gives you back an Authorization Code which you can exchange for a token. OAuth2 Authorization Code flow is meant to be interactive.

1 Like

That’s the issue - we are implementing single sign on so there is no user entry of username or password. It gets done via the SSO capabilities.
I need to be able to do this without user interaction.

If you are using Azure then this is what you want. The user does need to enter username / password if they aren’t already authenticated to Azure. If they are, it will be just seemless.

Here’s what that looks like 99% of the time
SSO

But if the user is somehow not Authenticated already to Azure then they’ll be re-directed there. Like this
SSO_NotLoggedIN

2 Likes

I wonder if we’re unintentionally talking past one another.

The OP is V10 on-prem IIS SSO in VBA and the discussion is in-browser Azure-SSO.

PS - Jose, I like it. Correct me if wrong, your Epicor is set to Azure auth directly, rather than IdP with Azure as external provider, correct? Do you know whether both can be done at the same time in Epicor cloud?

My use of the REST API is not web based. In the current, non-SSO environment I use the API to make BAQ calls to get data back for processing outside Epicor. Nothing at this point works in the cloud. We are on prem. I was hoping to be able to do an authorization call, then take the token and make my BAQ calls using that token. I feel like I am close but missing what I would call the “silent” authorization. It just sems like I am off by a parameter or 2.

Yes mine is setu0p witn Azure directly

As far as I know you can’t do both, only one External Idendity is available in Epicor itself.

This same flow works with on Premise Azure (its what I have)

1 Like