Category: Professional Projects

  • Using Go To Call Salesforce with the JWT Bearer OAuth Flow

    When using Go to call Salesforce’s OAuth with the JWT Bearer Flow you may see hard to resolve authentication errors if you are using the jtw.RegisteredClaims struct to build your JWT claims. We found that Salesforce requires the Audience claim to be a single string, for example "aud": "https://login.salesforce.com". The jwt package defaults to sending Registered Claims as an array of strings.

    This sample code shows how RegisteredClaims may be used to build a JWT. This JWT will fail authentication.

    claims := jwt.RegisteredClaims{
      Audience:  jwt.ClaimStrings{"https://login.salesforce.com"},
      Issuer:    "your-client-id",
      Subject:   "user@example.com",
      ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) // FAILS
    

    There are two neat solutions for this problem (and one very un-neat solution which Claude tried, and succeeded, when we encountered this). A simple solution is to use jwt.MapClaims. This solution lacks the type safety of RegisteredClaims

    claims := jwt.MapClaims{
      "aud": "https://login.salesforce.com",
      "iss": "your-client-id",
      "sub": "user@example.com",
      "exp": time.Now().Add(time.Hour).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    

    The RegisteredClaims package provides a global variable which, when set to false, will cause the library to marshal any single entry jwt.ClaimStrings as a string. This is a global setting, so lacks fine grain control. It worked in this situation because we only have one ClaimStrings.

    jwt.MarshalSingleStringAsArray = false
    
    claims := jwt.RegisteredClaims{
      Audience:  jwt.ClaimStrings{"https://login.salesforce.com"},
      Issuer:    "your-client-id",
      Subject:   "user@example.com",
      ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    

    Claude’s solution? Claude wrote a custom RegisteredClaims implementation which satisfied the required interfaces. This is longer code and works, but I wonder how maintainable it is especially as the library evolves.

    There are two security practices that I believe are important if you are doing this type of work:

    • Restrict the host names that you will send tokens to in order to login. If you are writing this as bespoke work and only have one org or always use login.salesforce.com and test.salesforce.com as your login domains then this is easy. Just hard-code these and provide a configuration switch between sandbox and production.
    • Trust the instance URL returned by Salesforce in their token response. This guarantees that API requests cannot be sent to the wrong host.

    Remember also that the JWT signing key and the access tokens that you receive on successful authentication are highly sensitive information. Compromise of either could be devastating.

    Resources