├── .forceignore ├── .gitignore ├── LICENSE ├── README.md ├── config └── project-scratch-def.json ├── force-app └── package │ ├── classes │ ├── OAuthJwtClientCredentials.cls │ ├── OAuthJwtClientCredentials.cls-meta.xml │ ├── OAuthJwtClientCredentialsTest.cls │ └── OAuthJwtClientCredentialsTest.cls-meta.xml │ ├── layouts │ └── OAuth_JWT_Client_Authentication__mdt-OAuth JWT Client Credential Layout.layout-meta.xml │ ├── objects │ └── OAuth_JWT_Client_Authentication__mdt │ │ ├── OAuth_JWT_Client_Authentication__mdt.object-meta.xml │ │ ├── fields │ │ ├── Additional_Token_Endpoint_Headers__c.field-meta.xml │ │ ├── Additional_Token_Endpoint_Parameters__c.field-meta.xml │ │ ├── Auth_Provider_Name__c.field-meta.xml │ │ ├── Custom_Callback_URL__c.field-meta.xml │ │ ├── Enable_Error_Logging__c.field-meta.xml │ │ ├── Enable_Login_History__c.field-meta.xml │ │ ├── Enable_Per_User_Login_Logging__c.field-meta.xml │ │ ├── Enable_Per_User_Mode__c.field-meta.xml │ │ ├── JWT_Algorithm__c.field-meta.xml │ │ ├── JWT_Audience__c.field-meta.xml │ │ ├── JWT_Issuer__c.field-meta.xml │ │ ├── JWT_Kid__c.field-meta.xml │ │ ├── JWT_Signing_Algorithm__c.field-meta.xml │ │ ├── JWT_Signing_Certificate_Name__c.field-meta.xml │ │ ├── JWT_Subject__c.field-meta.xml │ │ ├── Scope__c.field-meta.xml │ │ └── Token_Endpoint_URL__c.field-meta.xml │ │ └── validationRules │ │ └── Auth_Pr_Name_Needs_To_Match_URL_Suffix.validationRule-meta.xml │ └── permissionsets │ └── Lightweight_OAuth_JWT_Client_Credential_Auth_Provider.permissionset-meta.xml ├── media ├── 01_Auth_Provider.png ├── 02_Auth_Provider_Setup.png ├── 03_Remote_Site_Setting.png ├── 04_Permission_Set.png ├── 05_External_Credential.png ├── 06_Permission_Set_Mapping.png ├── 07_Authenticate.png ├── 08_Authentication_Success.png ├── 09_Named_Credential.png └── UserMapping.drawio ├── scripts ├── 00_security_scanner.bat ├── 01_package_dependencies.bat ├── 02_package_create_managed.bat ├── 03_package_create_unlocked.bat ├── 04_package_test.bat ├── 05_test_exceptions.apex ├── 06_test_successes.apex ├── 08_setup_connection.bat ├── 10_debug.apex ├── dependencies.bat └── package.bat └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** 13 | 14 | # Package related 15 | **/certs**/ 16 | **/objectTranslations/** 17 | **/profiles/** 18 | 19 | 20 | #**/authproviders/** 21 | #**/externalCredentials/** 22 | #**/namedCredentials/** 23 | #**/remoteSiteSettings/** 24 | #**/customMetadata/** 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | #Demo data for my own tests so api keys wont go public, even though they are dummy ones 6 | force-demo-data/ 7 | 8 | # Salesforce cache 9 | .sf/ 10 | .sfdx/ 11 | .vscode/ 12 | .localdevserver/ 13 | deploy-options.json 14 | 15 | # LWC VSCode autocomplete 16 | **/lwc/jsconfig.json 17 | 18 | # LWC Jest coverage reports 19 | coverage/ 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Dependency directories 29 | node_modules/ 30 | 31 | # Eslint cache 32 | .eslintcache 33 | 34 | # MacOS system files 35 | .DS_Store 36 | 37 | # Windows system files 38 | Thumbs.db 39 | ehthumbs.db 40 | [Dd]esktop.ini 41 | $RECYCLE.BIN/ 42 | 43 | # Local environment variables 44 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Justus van den Berg (jfwberg@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lightweight - OAuth 2.0 JWT Client Credentials Authentication Auth. Provider for use with Salesforce Named and External Credentials 2 | ## Description 3 | A reusable Auth Provider that can be used with named / external credentials that executes an OAuth 2.0 JWT Client Authentication flow using a Client Credentials grant type. 4 | 5 | The grant type standards are described in https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 6 | 7 | ## IMPORTANT UPDATE ## 8 | Since the Winter 24 release (API v59.0) Salesforce Named Credentials natively Support this flow making this Auth Provider Solution Obsolete for System Integrations using Named Principals. 9 | 10 | A reason to keep using this Auth Provider is you need a **Per User principal** based on the JWT Subject or send custom headers to the token endpoint. 11 | 12 | ## Blog details 13 | https://medium.com/@justusvandenberg/oauth-2-0-jwt-client-credentials-authentication-auth-d269835baae2 14 | 15 | ## Dependency - Package Info 16 | The following package need to be installed first before installing this package. (In this order) 17 | If you use the *managed package* you need to installed the managed package dependency and if you use the *unlocked version* you need to use the unlocked dependency. 18 | | Info | Value | 19 | |---|---| 20 | |Name|Lightweight - Apex Unit Test Util v2| 21 | |Version|2.4.0-1| 22 | |Managed Installation URL | */packaging/installPackage.apexp?p0=04tP3000000M6OXIA0* | 23 | |Unlocked Installation URL| */packaging/installPackage.apexp?p0=04tP3000000M6Q9IAK* | 24 | |Github URL | https://github.com/jfwberg/lightweight-apex-unit-test-util-v2 | 25 | | | | 26 | |Name|Lightweight - Apex REST Util| 27 | |Version|0.11.0-1| 28 | |Managed Installation URL | */packaging/installPackage.apexp?p0=04tP3000000M6gHIAS* | 29 | |Unlocked Installation URL| */packaging/installPackage.apexp?p0=04tP3000000M6htIAC* | 30 | |Github URL | https://github.com/jfwberg/lightweight-apex-rest-util | 31 | 32 | ## Optional Dependencies 33 | This package has an extension that adds a basic (error) logging functionality and a user mapping utility that allows the Auth Provider to work in a user context using "Per User" instead of "Named Principal". 34 | | Info | Value | 35 | |---|---| 36 | |Name|Lightweight - Auth Provider Util v2| 37 | |Version|0.12.0-1| 38 | |Managed Installation URL | */packaging/installPackage.apexp?p0=04tP3000000MVUzIAO* | 39 | |Unlocked Installation URL| */packaging/installPackage.apexp?p0=04tP3000000MW1FIAW* | 40 | |GIT URL | https://github.com/jfwberg/lightweight-auth-provider-util | 41 | 42 | ## Package info 43 | | Info | Value | 44 | |---|---| 45 | |Name|Lightweight - OAuth JWT Client Credentials Auth Provider| 46 | |Version|0.5.0-1| 47 | |Managed Installation URL | */packaging/installPackage.apexp?p0=04tP3000000MWfZIAW* | 48 | |Unlocked Installation URL| */packaging/installPackage.apexp?p0=04tP3000000MWndIAG* | 49 | 50 | ## Important 51 | - Security is no easy subject: Before implementing this (or any) solution, always validate what you're doing with a certified sercurity expert and your certified implementation partner 52 | - At the time of writing I work for Salesforce. The views / solutions presented here are strictly MY OWN and NOT per definition the views or solutions Salesforce would recommend. Again; always consult with your certified implementation partner before implementing anything. 53 | 54 | ## Pre-requisites 55 | - A certificate with private key that is used for signing the JWT with a JWS that is imported in the Salesforce certificate key store 56 | - Alternatively you can use a self signed certificate for testing purposes 57 | - The public key needs te be shared with the authorisation server and setup according to their standards, usually a JWKS 58 | - You'll need all the authorization server details that are required to setup the connection 59 | 60 | ## Assign permissions to Automated Process User 61 | Since the Spring 24 release platform events started running as the Automated Process User. Making the logging fail due to access issue. 62 | To fix this I created a specific permission set for this user that can be assigned using the code below. 63 | ```java 64 | insert new PermissionSetAssignment( 65 | AssigneeId = [SELECT Id FROM User WHERE alias = 'autoproc']?.Id, 66 | PermissionSetId = [SELECT Id FROM PermissionSet WHERE Name = 'Lightweight_Auth_Provider_Util_AutoProc']?.Id 67 | ); 68 | ``` 69 | 70 | ## 00 :: Deployment and Preparation 71 | 1. Import the JWT signing certificate into Salesforce, Note down the *Certificate API Name* 72 | 2. Deploy the *Apex class* and the *Custom Metadata (including layouts)* from this SFDX Pproject to your Org (Or install the package) 73 | 3. The package can be found here: *https://[MY_DOMAIN_URL]/packaging/installPackage.apexp?p0=* 74 | 75 | ## 01 :: Setup the Auth. Provider 76 | In my example I am going to connect an api called "PiMoria"; this my test domain that I will use throughout this example. 77 | 78 | 1. In setup > Auth Providers > Create a new Auth. Provider Using the *OAuthJwtClientCredentials* class as the type 79 | ![image info](./media/01_Auth_Provider.png) 80 | 81 | 2. Populate the *Execute Registration As* field first, this is a mandatory field that is not marked as mandatory and will reset the entire form if you forget it. 82 | 3. Populate the fields in the the Auth. Provider. The below table details what is required in the fields 83 | 4. Triple check you put in all mandatory fields: If you have forgotten one, you have to re-do them all 84 | 85 | | Field Name | Description | Example | 86 | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------| 87 | | Name | Auth Provider API Name | PiMoria PROD | 88 | | URL Suffix | The URL suffix that is used in the callback URL Make sure this is the same as the name field | PiMoria | 89 | | Additional Token Endpoint Headers | Optional headers that are send during the API token request. Key value pairs are split with a comma and header key values are set using a colon | apiId : echo, apiKey : 1919 | 90 | | Additional Token Endpoint Parameters | Optional POST body parameters that are send during the API token request. Key value pairs are split with a comma and parameter key values are set using a colon | tenant : 12-345, client_id : ab-cde | 91 | | Auth Provider Name | The name of the Auth Provider: !! This must be the same as the Name field !! | PiMora | 92 | | JWT Algorithm | The algoritm used for signing the JWT. Valid values are: 'RS256','RS384','RS512','ES256','ES384','ES512' Note: we are limited to the algorithms supported by the Crypto Class | RS512 | 93 | | JWT Audience | The aud in the JWT | https://prod.pimoria.com | 94 | | JWT Issuer | The iss in the JWT | pimoria-client-api-identifier | 95 | | JWT Kid | The Key Id in the JWT | prod-1 | 96 | | JWT Signing Algorithm | The algorithm used to sign the JWT and generate a JWS 'RSA-SHA256','RSA-SHA384','RSA-SHA512','ECDSA-SHA256','ECDSA-SHA384','ECDSA-SHA512' | RSA-SHA512 | 97 | | JWT Signing Certificate Name | The certificate API name that is used for signing the certificate | PiMoriaProd | 98 | | JWT Subject | The sub field in the JWT | system.user@pimoria.com | 99 | | Token Endpoint URL | The URL for the token endpoint, usually ends in /oauth2/token | https://prod.pimoria.com/oauth2/token| 100 | | Scope | Optional value for the scope parameter in the request body | api,refresh_token | 101 | | Custom Callback URL | Optionally you can add your custom callback URL, this should not be required. The code generates the callback URL based on the name | | 102 | | Enable Error Logging | Optionally error logging can be enabled to log any errors thrown during the get token process | | 103 | | Enable Per User Mode | Optionally the Auth Provider allows for Per User mode instead of Named Principal, to allow for user APIs that require user context. A mapping needs to be setup. | | 104 | | Enable Per User Login Logging | Optionally you can add your custom callback URL, this should not be required. The code generates the callback URL based on the name | | 105 | 106 | 107 | When finished it should look like something like this 108 | ![image info](./media/02_Auth_Provider_Setup.png) 109 | 110 | ## 02 :: Setup The Remote Site Setting(s) 111 | In order to make API call-outs to the token endpoint securely, we must setup a remote site setting for the token endpoint. 112 | 113 | 1. Go to Setup > Security > Remote Site Settings and click *New* 114 | 2. Populate the *Remote Site URL* field with the *Token Endpoint Base URL* and set a *Name* and *Description* 115 | 3. Press *Save* 116 | 4. If your API Base URL is different than your token URL, you need to create separate Remote Site Setting for this API. In my example case, the base URL is the same. 117 | ![image info](./media/03_Remote_Site_Setting.png) 118 | 119 | 120 | ## 03 :: Create a Permission Set for the External Credential 121 | External Credentials require a Permission Set in order to create a credential type mapping. It's best practice to create a separate Permisison Set for each Extern Credential to keep a strict separation and stick to the least access principle. 122 | At this stage you don't have to assign any permissions, this will happen later. 123 | 1. In Setup > Users > Permission Sets, Click *New* 124 | 2. Set a *Label* and an *API Name*, write the API Name down, we're going to need this later. 125 | 3. Assign the Permission Set to the testing user, probably the user you're logged in with 126 | ![image info](./media/04_Permission_Set.png) 127 | 128 | ## 04 :: Setup the External Credential 129 | Your Auth Provider is now ready for testing. The next step is to create an External Credential that authenticates to the token endpoint using the Auth Provider. 130 | 131 | 1. Go to setup > Security > Named Credentials and click the *External Credentials tab* 132 | 2. Click *New*, Set A Label and a Name 133 | 3. Set *Authentication Protocol* to *OAuth 2.0* 134 | 4. Set *Authentication Flow Type* to *Browser Flow*. The *Scope* field can be left blank. This is overwritten by our Auth. Provider Settings. 135 | 5. Select your created Auth. Provider from the *Auth Provider Picklist* 136 | 6. Press Save 137 | ![image info](./media/05_External_Credential.png) 138 | 7. Scroll down to the *Permission Set Mappings* section and press *New* 139 | 8. For the *Permission Set*, select the Permission Set You created in Step 3.1. Set the *Identity Type* field to *Named Principal*. You can ignore the sequence number. This can stay default. 140 | 9. Press Save 141 | ![image info](./media/06_Permission_Set_Mapping.png) 142 | 143 | ## 05 Connect your External Credential through the Auth Provider 144 | You have no finished setting up the external credetials and the auth provider it's time to test it by callign the token endpoint and authenticate. 145 | 146 | 1. Click on the arrow button next to the Permission Set Mapping in the actions column 147 | 2. Click *Authenticate*, Salesforce now call the token endpoint and execute the logic from the Apex class to get a token. It will redirect you to the same page (This is the redirect URL that is auto generated in the class.) 148 | ![image info](./media/07_Authenticate.png) 149 | 3. If you're successful you get a sucess message, if you're unsuccesful you have to start debugging. 150 | ![image info](./media/08_Authentication_Success.png) 151 | 152 | 153 | ## 06 Debugging and Common Errors 154 | * Forgotten to set the Remote Site Settings 155 | * Forgotten to assign the permission set 156 | * Wrong certificate name (Note this is Case Sensitive, you get a "System.NoDataFoundException: Data Not Available" exception if the certificate API Name is wrong) 157 | * Any other (JWT) configuration details. If this is the case the debug logs with show the response as an *OAuthJwtClientCredentials.TokenException* will be thrown. 158 | I don't want to go to deep into debugging but a few pointers: 159 | * Set the trace flag on the executing user 160 | * Clean your debug logs in setup, execute the code and see the logs 161 | * Alternatively, open your developer console to stream the logs 162 | 163 | ## 07 Create a Named Credential 164 | Once you have successfully authenticated your external credential and you want to use this connection from Apex or Flows to call an API we'll need a Named Credential 165 | 1. Go to setup > Security > Named Credentials and click the *NAmed Credentials tab* 166 | 2. Click *New* (Note we are NOT creating a legacy named credential, we're modern) 167 | 3. Give your credential a *Label* and a *Name* 168 | 4. Populate the *URL* field with the base URL of the API (Not the token endpoint). Quite often these are the same. 169 | 5. Select your *External Credential* you created in step 4.2 170 | 6. Make sure *Generate Authorization Header* is selected, this is by default 171 | 7. Optionally you can select a namespace and a network if you use prive connect. 172 | 8. Press Save 173 | ![image info](./media/09_Named_Credential.png) 174 | 175 | ## 08 Test the Named Credential 176 | Now you have everything you need. Let's open up and execute anonymous window and call one of our endpoints. 177 | * If there is an invalid token a 401 response code will be received. If this happens the external credential will call the refresh token logic through the Auth Provider Automatically and take care of all of that overhead. 178 | * *callout:[NamedCredentialApiNAme]:* will be replaced by the base URL as specified in the step 7.4. 179 | * Update and run the following code snippet and you should be able to successfully call the API. 180 | 181 | ```java 182 | HttpRequest request = new HttpRequest(); 183 | request.setEndPoint('callout:PiMoria/api/authentication-test'); 184 | request.setMethod('POST'); 185 | HttpResponse response = new HTTP().send(request); 186 | System.debug(response.getBody()); 187 | ``` 188 | ## Note on coding 189 | - Everything is kept in a single class, to make it small and stand-alone. This includes any validations that could have been in validation rules or messages that could have been in custom labels. This is a contious design decision to keep everything together. 190 | - Any confguration values should be in a constant at the top following the common code structure 191 | - Always use the this keyword properly for readability 192 | - Always add ApexDoc headers even if it seems overkill, it's just good practice and easy to document 193 | - Certificate related methods cannot be tested because Apex cannot mock certificates. The alternative is to supply a certificate name in the test class but I'd like to keep the tests org agnostic 194 | - It's OK to use the @SuppressWarnings annotation but always mention the false-positives 195 | 196 | ## Steps to import a certificate store (JKS) in a Scratch Org when getting "Data Not Available" error message on import 197 | In some cases there is a bug in the certificate import that gives an error if you try to import a JKS It says "Data Not Available". 198 | I have this issue in all my scratch orgs. There is a simple way to resolve this. 199 | 1) Go to setup > certificate and key management 200 | 2) Create a self signed certficate 201 | 3) Go to Setup >> Identity provider 202 | 4) Click enable Identity Provider and select the self signed certificate you just created and press save 203 | 5) Press the disable button, as we dont really need it 204 | 6) Go back to Setup > Certificate and Key Management and try to "import from keystore" again, it should work now. 205 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider - Dev Scratch Org", 3 | "edition": "Enterprise", 4 | "features": [ 5 | "EnableSetPasswordInApi", 6 | "PersonAccounts", 7 | "PlatformCache", 8 | "PlatformEncryption", 9 | "EinsteinGPTForDevelopers" 10 | ], 11 | "language": "en_US", 12 | "country": "GB", 13 | "settings": { 14 | "lightningExperienceSettings": { 15 | "enableS1DesktopEnabled": true 16 | }, 17 | "mobileSettings": { 18 | "enableS1EncryptedStoragePref2": false 19 | }, 20 | "securitySettings": { 21 | "canUsersGrantLoginAccess": false, 22 | "enableAdminLoginAsAnyUser": false, 23 | "sessionSettings": { 24 | "sessionTimeout": "TwentyFourHours" 25 | }, 26 | "passwordPolicies": { 27 | "expiration": "Never", 28 | "historyRestriction": "0" 29 | }, 30 | "singleSignOnSettings": { 31 | "enableSamlLogin": true 32 | }, 33 | "networkAccess": { 34 | "ipRanges": [ 35 | { 36 | "end": "255.255.255.255", 37 | "start": "254.0.0.0" 38 | }, 39 | { 40 | "end": "253.255.255.255", 41 | "start": "252.0.0.0" 42 | }, 43 | { 44 | "end": "251.255.255.255", 45 | "start": "250.0.0.0" 46 | }, 47 | { 48 | "end": "249.255.255.255", 49 | "start": "248.0.0.0" 50 | }, 51 | { 52 | "end": "247.255.255.255", 53 | "start": "246.0.0.0" 54 | }, 55 | { 56 | "end": "245.255.255.255", 57 | "start": "244.0.0.0" 58 | }, 59 | { 60 | "end": "243.255.255.255", 61 | "start": "242.0.0.0" 62 | }, 63 | { 64 | "end": "241.255.255.255", 65 | "start": "240.0.0.0" 66 | }, 67 | { 68 | "end": "239.255.255.255", 69 | "start": "238.0.0.0" 70 | }, 71 | { 72 | "end": "237.255.255.255", 73 | "start": "236.0.0.0" 74 | }, 75 | { 76 | "end": "235.255.255.255", 77 | "start": "234.0.0.0" 78 | }, 79 | { 80 | "end": "233.255.255.255", 81 | "start": "232.0.0.0" 82 | }, 83 | { 84 | "end": "231.255.255.255", 85 | "start": "230.0.0.0" 86 | }, 87 | { 88 | "end": "229.255.255.255", 89 | "start": "228.0.0.0" 90 | }, 91 | { 92 | "end": "227.255.255.255", 93 | "start": "226.0.0.0" 94 | }, 95 | { 96 | "end": "225.255.255.255", 97 | "start": "224.0.0.0" 98 | }, 99 | { 100 | "end": "223.255.255.255", 101 | "start": "222.0.0.0" 102 | }, 103 | { 104 | "end": "221.255.255.255", 105 | "start": "220.0.0.0" 106 | }, 107 | { 108 | "end": "219.255.255.255", 109 | "start": "218.0.0.0" 110 | }, 111 | { 112 | "end": "217.255.255.255", 113 | "start": "216.0.0.0" 114 | }, 115 | { 116 | "end": "215.255.255.255", 117 | "start": "214.0.0.0" 118 | }, 119 | { 120 | "end": "213.255.255.255", 121 | "start": "212.0.0.0" 122 | }, 123 | { 124 | "end": "211.255.255.255", 125 | "start": "210.0.0.0" 126 | }, 127 | { 128 | "end": "209.255.255.255", 129 | "start": "208.0.0.0" 130 | }, 131 | { 132 | "end": "207.255.255.255", 133 | "start": "206.0.0.0" 134 | }, 135 | { 136 | "end": "205.255.255.255", 137 | "start": "204.0.0.0" 138 | }, 139 | { 140 | "end": "203.255.255.255", 141 | "start": "202.0.0.0" 142 | }, 143 | { 144 | "end": "201.255.255.255", 145 | "start": "200.0.0.0" 146 | }, 147 | { 148 | "end": "199.255.255.255", 149 | "start": "198.0.0.0" 150 | }, 151 | { 152 | "end": "197.255.255.255", 153 | "start": "196.0.0.0" 154 | }, 155 | { 156 | "end": "195.255.255.255", 157 | "start": "194.0.0.0" 158 | }, 159 | { 160 | "end": "193.255.255.255", 161 | "start": "192.0.0.0" 162 | }, 163 | { 164 | "end": "191.255.255.255", 165 | "start": "190.0.0.0" 166 | }, 167 | { 168 | "end": "189.255.255.255", 169 | "start": "188.0.0.0" 170 | }, 171 | { 172 | "end": "187.255.255.255", 173 | "start": "186.0.0.0" 174 | }, 175 | { 176 | "end": "185.255.255.255", 177 | "start": "184.0.0.0" 178 | }, 179 | { 180 | "end": "183.255.255.255", 181 | "start": "182.0.0.0" 182 | }, 183 | { 184 | "end": "181.255.255.255", 185 | "start": "180.0.0.0" 186 | }, 187 | { 188 | "end": "179.255.255.255", 189 | "start": "178.0.0.0" 190 | }, 191 | { 192 | "end": "177.255.255.255", 193 | "start": "176.0.0.0" 194 | }, 195 | { 196 | "end": "175.255.255.255", 197 | "start": "174.0.0.0" 198 | }, 199 | { 200 | "end": "173.255.255.255", 201 | "start": "172.0.0.0" 202 | }, 203 | { 204 | "end": "171.255.255.255", 205 | "start": "170.0.0.0" 206 | }, 207 | { 208 | "end": "169.255.255.255", 209 | "start": "168.0.0.0" 210 | }, 211 | { 212 | "end": "167.255.255.255", 213 | "start": "166.0.0.0" 214 | }, 215 | { 216 | "end": "165.255.255.255", 217 | "start": "164.0.0.0" 218 | }, 219 | { 220 | "end": "163.255.255.255", 221 | "start": "162.0.0.0" 222 | }, 223 | { 224 | "end": "161.255.255.255", 225 | "start": "160.0.0.0" 226 | }, 227 | { 228 | "end": "159.255.255.255", 229 | "start": "159.0.0.0" 230 | }, 231 | { 232 | "end": "157.255.255.255", 233 | "start": "156.0.0.0" 234 | }, 235 | { 236 | "end": "155.255.255.255", 237 | "start": "154.0.0.0" 238 | }, 239 | { 240 | "end": "153.255.255.255", 241 | "start": "152.0.0.0" 242 | }, 243 | { 244 | "end": "151.255.255.255", 245 | "start": "150.0.0.0" 246 | }, 247 | { 248 | "end": "149.255.255.255", 249 | "start": "148.0.0.0" 250 | }, 251 | { 252 | "end": "147.255.255.255", 253 | "start": "146.0.0.0" 254 | }, 255 | { 256 | "end": "145.255.255.255", 257 | "start": "144.0.0.0" 258 | }, 259 | { 260 | "end": "143.255.255.255", 261 | "start": "142.0.0.0" 262 | }, 263 | { 264 | "end": "141.255.255.255", 265 | "start": "140.0.0.0" 266 | }, 267 | { 268 | "end": "139.255.255.255", 269 | "start": "138.0.0.0" 270 | }, 271 | { 272 | "end": "137.255.255.255", 273 | "start": "136.0.0.0" 274 | }, 275 | { 276 | "end": "135.255.255.255", 277 | "start": "134.0.0.0" 278 | }, 279 | { 280 | "end": "133.255.255.255", 281 | "start": "132.0.0.0" 282 | }, 283 | { 284 | "end": "131.255.255.255", 285 | "start": "130.0.0.0" 286 | }, 287 | { 288 | "end": "129.255.255.255", 289 | "start": "128.0.0.0" 290 | }, 291 | { 292 | "end": "127.255.255.255", 293 | "start": "126.0.0.0" 294 | }, 295 | { 296 | "end": "125.255.255.255", 297 | "start": "124.0.0.0" 298 | }, 299 | { 300 | "end": "123.255.255.255", 301 | "start": "122.0.0.0" 302 | }, 303 | { 304 | "end": "121.255.255.255", 305 | "start": "120.0.0.0" 306 | }, 307 | { 308 | "end": "119.255.255.255", 309 | "start": "118.0.0.0" 310 | }, 311 | { 312 | "end": "117.255.255.255", 313 | "start": "116.0.0.0" 314 | }, 315 | { 316 | "end": "115.255.255.255", 317 | "start": "114.0.0.0" 318 | }, 319 | { 320 | "end": "113.255.255.255", 321 | "start": "112.0.0.0" 322 | }, 323 | { 324 | "end": "111.255.255.255", 325 | "start": "110.0.0.0" 326 | }, 327 | { 328 | "end": "109.255.255.255", 329 | "start": "108.0.0.0" 330 | }, 331 | { 332 | "end": "107.255.255.255", 333 | "start": "106.0.0.0" 334 | }, 335 | { 336 | "end": "105.255.255.255", 337 | "start": "104.0.0.0" 338 | }, 339 | { 340 | "end": "103.255.255.255", 341 | "start": "102.0.0.0" 342 | }, 343 | { 344 | "end": "101.255.255.255", 345 | "start": "100.0.0.0" 346 | }, 347 | { 348 | "end": "99.255.255.255", 349 | "start": "98.0.0.0" 350 | }, 351 | { 352 | "end": "97.255.255.255", 353 | "start": "96.0.0.0" 354 | }, 355 | { 356 | "end": "95.255.255.255", 357 | "start": "94.0.0.0" 358 | }, 359 | { 360 | "end": "93.255.255.255", 361 | "start": "92.0.0.0" 362 | }, 363 | { 364 | "end": "91.255.255.255", 365 | "start": "90.0.0.0" 366 | }, 367 | { 368 | "end": "89.255.255.255", 369 | "start": "88.0.0.0" 370 | }, 371 | { 372 | "end": "87.255.255.255", 373 | "start": "86.0.0.0" 374 | }, 375 | { 376 | "end": "85.255.255.255", 377 | "start": "84.0.0.0" 378 | }, 379 | { 380 | "end": "83.255.255.255", 381 | "start": "82.0.0.0" 382 | }, 383 | { 384 | "end": "81.255.255.255", 385 | "start": "80.0.0.0" 386 | }, 387 | { 388 | "end": "79.255.255.255", 389 | "start": "78.0.0.0" 390 | }, 391 | { 392 | "end": "77.255.255.255", 393 | "start": "76.0.0.0" 394 | }, 395 | { 396 | "end": "75.255.255.255", 397 | "start": "74.0.0.0" 398 | }, 399 | { 400 | "end": "73.255.255.255", 401 | "start": "72.0.0.0" 402 | }, 403 | { 404 | "end": "71.255.255.255", 405 | "start": "70.0.0.0" 406 | }, 407 | { 408 | "end": "69.255.255.255", 409 | "start": "68.0.0.0" 410 | }, 411 | { 412 | "end": "67.255.255.255", 413 | "start": "66.0.0.0" 414 | }, 415 | { 416 | "end": "65.255.255.255", 417 | "start": "64.0.0.0" 418 | }, 419 | { 420 | "end": "63.255.255.255", 421 | "start": "62.0.0.0" 422 | }, 423 | { 424 | "end": "61.255.255.255", 425 | "start": "60.0.0.0" 426 | }, 427 | { 428 | "end": "59.255.255.255", 429 | "start": "59.0.0.0" 430 | }, 431 | { 432 | "end": "57.255.255.255", 433 | "start": "56.0.0.0" 434 | }, 435 | { 436 | "end": "55.255.255.255", 437 | "start": "54.0.0.0" 438 | }, 439 | { 440 | "end": "53.255.255.255", 441 | "start": "52.0.0.0" 442 | }, 443 | { 444 | "end": "51.255.255.255", 445 | "start": "50.0.0.0" 446 | }, 447 | { 448 | "end": "49.255.255.255", 449 | "start": "48.0.0.0" 450 | }, 451 | { 452 | "end": "47.255.255.255", 453 | "start": "46.0.0.0" 454 | }, 455 | { 456 | "end": "45.255.255.255", 457 | "start": "44.0.0.0" 458 | }, 459 | { 460 | "end": "43.255.255.255", 461 | "start": "42.0.0.0" 462 | }, 463 | { 464 | "end": "41.255.255.255", 465 | "start": "40.0.0.0" 466 | }, 467 | { 468 | "end": "39.255.255.255", 469 | "start": "38.0.0.0" 470 | }, 471 | { 472 | "end": "37.255.255.255", 473 | "start": "36.0.0.0" 474 | }, 475 | { 476 | "end": "35.255.255.255", 477 | "start": "34.0.0.0" 478 | }, 479 | { 480 | "end": "33.255.255.255", 481 | "start": "32.0.0.0" 482 | }, 483 | { 484 | "end": "31.255.255.255", 485 | "start": "30.0.0.0" 486 | }, 487 | { 488 | "end": "29.255.255.255", 489 | "start": "28.0.0.0" 490 | }, 491 | { 492 | "end": "27.255.255.255", 493 | "start": "26.0.0.0" 494 | }, 495 | { 496 | "end": "25.255.255.255", 497 | "start": "24.0.0.0" 498 | }, 499 | { 500 | "end": "23.255.255.255", 501 | "start": "22.0.0.0" 502 | }, 503 | { 504 | "end": "21.255.255.255", 505 | "start": "20.0.0.0" 506 | }, 507 | { 508 | "end": "19.255.255.255", 509 | "start": "18.0.0.0" 510 | }, 511 | { 512 | "end": "17.255.255.255", 513 | "start": "16.0.0.0" 514 | }, 515 | { 516 | "end": "15.255.255.255", 517 | "start": "14.0.0.0" 518 | }, 519 | { 520 | "end": "13.255.255.255", 521 | "start": "12.0.0.0" 522 | }, 523 | { 524 | "end": "11.255.255.255", 525 | "start": "10.0.0.0" 526 | }, 527 | { 528 | "end": "9.255.255.255", 529 | "start": "8.0.0.0" 530 | }, 531 | { 532 | "end": "7.255.255.255", 533 | "start": "6.0.0.0" 534 | }, 535 | { 536 | "end": "5.255.255.255", 537 | "start": "4.0.0.0" 538 | }, 539 | { 540 | "end": "3.255.255.255", 541 | "start": "2.0.0.0" 542 | }, 543 | { 544 | "end": "1.255.255.255", 545 | "start": "0.0.0.0" 546 | } 547 | ] 548 | } 549 | } 550 | } 551 | } -------------------------------------------------------------------------------- /force-app/package/classes/OAuthJwtClientCredentials.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Justus van den Berg (jfwberg@gmail.com) 3 | * @date April 2023 4 | * @copyright (c) 2024 Justus van den Berg 5 | * @license MIT (See LICENSE file in the project root) 6 | * @description OAuth 2.0 JWT Client Authentication Auth Provider for the use with Client Credentials 7 | * Follows the standard as described in rfc7523Section 2.2 8 | * (https://datatracker.ietf.org/doc/html/rfc7523#section-2.2) 9 | * @note In order to use the (error) logging functions and to enabled "Per User" mode for 10 | * user context mappings, It is required that the "Lightweight - Auth Provider Util 11 | * v2" (04t4K000002Jv1tQAC) package is installed. 12 | * @change Implemented the following new functions: 13 | * - Optional parameters, azure requires ?client_id=[client_id]&tentant=abcede 14 | * - Error logging 15 | * - Per user principal as well as Named Principal 16 | * - User login logging, to keep track of what users have requested tokens 17 | * @false-positive PMD.CyclomaticComplexity The Cyclomatic Complexity in this class is quite high. 18 | * The methods themselves are OK. 19 | * It's due to the fact that I want al the logic in a 20 | * single class, as an AuthProvider the class has a single 21 | * function and is used in a single place. 22 | * It can be split up, but this keeps it nicely together 23 | * and the class just under 800 lines what is acceptable 24 | * imho. 25 | */ 26 | @SuppressWarnings('PMD.CyclomaticComplexity') 27 | public with sharing class OAuthJwtClientCredentials extends Auth.AuthProviderPluginClass{ 28 | 29 | /** **************************************************************************************************** ** 30 | ** PRIVATE VARIABLES ** 31 | ** **************************************************************************************************** **/ 32 | // For debugging purposes extract the callout data 33 | @TestVisible 34 | private utl.Rst tokenCallout; 35 | 36 | // The optional auth provider util for error logging and per user principal (requires the "Lightweight - Auth Provider Util v2" package) 37 | // If you dont want this depencency, disable the options "Enable Error Logging", "Enabled Per user principal" and "Enabled Per User Login Logging" in the Custom Auth Provider Setup. 38 | private static Callable authProviderUtil; 39 | 40 | // The details of the logged in user 41 | @TestVisible private String loggedInUserId = UserInfo.getUserId(); 42 | @TestVisible private Auth.UserData loggedInUserData = new Auth.UserData( 43 | UserInfo.getUserId(), // User Id 44 | UserInfo.getFirstName(), // First Name 45 | UserInfo.getLastName(), // Last Name 46 | UserInfo.getFirstName() + ' ' + UserInfo.getLastName(), // Full Name (user name here) 47 | UserInfo.getUserEmail(), // Email Address 48 | null, // Link 49 | UserInfo.getUserName(), // Username 50 | UserInfo.getLocale(), // Locale 51 | null, // Provider 52 | null, // Site login 53 | null // Attribute map 54 | ); 55 | 56 | 57 | /** **************************************************************************************************** ** 58 | ** PRIVATE CONSTANTS ** 59 | ** **************************************************************************************************** **/ 60 | // Grant details 61 | @TestVisible private final static String SCOPE_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Scope__c); 62 | @TestVisible private final static String TOKEN_ENDPOINT_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Token_Endpoint_URL__c); 63 | @TestVisible private final static String TOKEN_HEADERS_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Additional_Token_Endpoint_Headers__c); 64 | @TestVisible private final static String TOKEN_PARAMS_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Additional_Token_Endpoint_Parameters__c); 65 | @TestVisible private final static String CUSTOM_CALLBACK_FIELD_NAME= String.valueOf(OAuth_JWT_Client_Authentication__mdt.Custom_Callback_URL__c); 66 | 67 | // JWT Header info 68 | @TestVisible private final static String JWT_ALGORITHM_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Algorithm__c); 69 | @TestVisible private final static String JWT_KID_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Kid__c); 70 | 71 | // JWT Settings 72 | @TestVisible private final static String JWT_SUBJECT_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Subject__c); 73 | @TestVisible private final static String JWT_ISSUER_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Issuer__c); 74 | @TestVisible private final static String JWT_AUDIENCE_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Audience__c); 75 | 76 | // Specify the name for your auth provider for the callback URL 77 | @TestVisible private final static String AUTH_PROVIDER_NAME_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Auth_Provider_Name__c); 78 | 79 | // The API name of the certificate and the algorithm used for signing the JWT 80 | @TestVisible private final static String JWS_SIGNING_CERT_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Signing_Certificate_Name__c); 81 | @TestVisible private final static String JWS_SIGNING_ALGORITHM_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.JWT_Signing_Algorithm__c); 82 | 83 | // Setup switches 84 | @TestVisible private final static String ENABLE_PER_USER_MODE_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Enable_Per_User_Mode__c); 85 | @TestVisible private final static String ENABLE_ERROR_LOGGING_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Enable_Error_Logging__c); 86 | @TestVisible private final static String ENABLE_LOGIN_LOGGING_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Enable_Per_User_Login_Logging__c); 87 | @TestVisible private final static String ENABLE_LOGIN_HISTORY_FIELD_NAME = String.valueOf(OAuth_JWT_Client_Authentication__mdt.Enable_Login_History__c); 88 | 89 | // This will generate the GUID that is used to identify this specific transaction so it can be followed through the logs 90 | @TestVisible private final static String GUID = UUID.randomUUID().toString(); 91 | 92 | // Parameter names 93 | @TestVisible private final static String PARAM_NAME_STATE = 'state'; 94 | 95 | // Test Cookie header 96 | @TestVisible private final static String TEST_COOKIE_HEADER = 'sid=[SESSION_ID];'; 97 | @TestVisible private final static String TEST_ASSERTION = '[TEST_ASSERTION]'; 98 | 99 | 100 | // Valid algorithm for JKS Header and JWT Signing Certificate for validating the user inputs based on availible SFDC functionality 101 | @TestVisible private final static Set VALID_JWS_HEADER_ALGORITHMS = new Set{'RS256','RS384','RS512','ES256','ES384','ES512'}; 102 | @TestVisible private final static Set VALID_JWS_SIGNING_ALGORITHMS = new Set{'RSA-SHA256','RSA-SHA384','RSA-SHA512','ECDSA-SHA256','ECDSA-SHA384','ECDSA-SHA512'}; 103 | 104 | // Any messages go here 105 | @TestVisible private final static String GENERIC_EXCEPTION_MSG = 'A {0} was thrown with the message: {1}'; 106 | @TestVisible private final static String NO_USER_MAPPING_MSG = 'Nu User Mapping Record was found for Auth Proivder "{0}" with user "{1}"'; 107 | @TestVisible private final static String MISSING_UTIL_PACKAGE_MSG = 'Issue whilst instantiating the AuthProviderUtil class. Make sure the "Lightweight - Auth Provider Util v2" package is installed. Alternatively, DISABLE the options "Enable Error Logging", "Enable Per user principal" and "Enable Per User Login Logging" in the Custom Auth Provider Setup.'; 108 | @TestVisible private final static String INVALID_TOKEN_RESPONSE_MSG = 'Unexpected response when calling the token endpoint: {0}'; 109 | @TestVisible private final static String JWS_INVALID_HEAD_ALG_EXCEPTION_MSG= 'Invalid JWS Header Algorithm provided. Valid values are: \'RS256\',\'RS384\',\'RS512\',\'ES256\',\'ES384\' and \'ES512\''; 110 | @TestVisible private final static String JWS_INVALID_SIGN_ALG_EXCEPTION_MSG= 'Invalid JWS Signing Algorithm provided. Valid values are: \'RSA-SHA256\',\'RSA-SHA384\',\'RSA-SHA512\',\'ECDSA-SHA256\',\'ECDSA-SHA384\',\'ECDSA-SHA512\''; 111 | 112 | 113 | /** **************************************************************************************************** ** 114 | ** PUBLIC INTERFACE METHODS ** 115 | ** **************************************************************************************************** **/ 116 | /** 117 | * @description Returns the URL where the user is redirected for authentication. 118 | * @param authProviderConfiguration The configuration items for the custom authentication 119 | * provider that have been configured in the custom 120 | * metadata type. 121 | * @param stateToPropagate The state passed in to initiate the authentication 122 | * request for the user 123 | * @return The URL of the page where the user is redirected for authentication. 124 | * @false-positives The URL is generated at a known source and no danger. It's not user updateable. 125 | * Also the remote site settings will prevent any unauthorised endpoint call-outs 126 | */ 127 | @SuppressWarnings('PMD.ApexOpenRedirect') 128 | public PageReference initiate(Map authProviderConfiguration, String stateToPropagate){ 129 | 130 | // Get the standard auth provider endpoint url 131 | PageReference pageReference = this.getSfdcCallbackURL(authProviderConfiguration); 132 | 133 | // Add the state parameter 134 | pageReference.getParameters().put(PARAM_NAME_STATE, stateToPropagate); 135 | 136 | // Return the pageReference 137 | return pageReference; 138 | } 139 | 140 | 141 | /** 142 | * @description Uses the authentication provider’s supported authentication protocol to return an 143 | * OAuth access token, OAuth secret or refresh token, and the state passed in when the 144 | * request for the current user was initiated. 145 | * @param authProviderConfiguration The configuration items for the custom authentication 146 | * provider that have been configured in the custom metadata 147 | * type. 148 | * @param callbackState The class that contains the HTTP headers, body, and 149 | * queryParams of the authentication request. 150 | * @return Creates an instance of the AuthProviderTokenResponse class 151 | * @note There is no refresh token in the OAUth 2.0 JWT Client Authentication flow so we 152 | * just ignore this value or put in a random, invalid value. 153 | */ 154 | public Auth.AuthProviderTokenResponse handleCallback(Map authProviderConfiguration, Auth.AuthProviderCallbackState callbackState){ 155 | try{ 156 | // Check if the "per user" principal is enabled and if so set the logged in user Id 157 | if(Boolean.valueOf(authProviderConfiguration.get(ENABLE_PER_USER_MODE_FIELD_NAME))){ 158 | this.setLoggedInUserDetails(callbackState); 159 | } 160 | 161 | // Retrieve a new token from the token endpoint 162 | TokenResponse tokenResponse = this.retrieveToken(authProviderConfiguration); 163 | 164 | // Manage the login history 165 | this.handleInsertHistoryRecord( 166 | authProviderConfiguration, 167 | 'Initial', 168 | String.isNotBlank(tokenResponse.access_token), 169 | 'JWT_CLIENT_CREDENTIALS', 170 | this.tokenCallout 171 | ); 172 | 173 | 174 | // Check if the per user principal is enabled, if so update the log entry for that user mapping 175 | if( Boolean.valueOf(authProviderConfiguration.get(ENABLE_PER_USER_MODE_FIELD_NAME)) && 176 | Boolean.valueOf(authProviderConfiguration.get(ENABLE_LOGIN_LOGGING_FIELD_NAME))){ 177 | 178 | // Update the mapping record 179 | getAuthProviderUtil().call('updateMappingLoginDetails', new Map { 180 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 181 | 'userId' => loggedInUserId 182 | }); 183 | } 184 | 185 | // Error with the token, purposely thrown here to manage proper error logging 186 | if(String.isBlank(tokenResponse.access_token)){ 187 | throw new TokenException(String.format(INVALID_TOKEN_RESPONSE_MSG, new String[]{tokenCallout.getResponse().getBody()})); 188 | } 189 | 190 | 191 | // Return the the token response, there is no refresh token so we just set a random value 192 | return new Auth.AuthProviderTokenResponse( 193 | authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 194 | tokenResponse.access_token, 195 | loggedInUserId, 196 | callbackState.queryParameters.get(PARAM_NAME_STATE) 197 | ); 198 | }catch(Exception e){ 199 | handleException(e, authProviderConfiguration); 200 | } 201 | 202 | // Unreachable statement to please the apex compiler 203 | return null; 204 | } 205 | 206 | 207 | /** 208 | * @description Returns a new access token, which is used to update an expired access token. 209 | * @param authProviderConfiguration The configuration items for the custom authentication 210 | * provider that have been configured in the custom metadata 211 | * type. 212 | * @param refreshToken The refresh token for the user who is logged in. 213 | * @return Returns the new access token, or an error message if an error occurs. 214 | * @note There is no refresh token in the OAUth 2.0 JWT Client Authentication flow so we 215 | * just ignore this value or put in a random, invalid value. 216 | */ 217 | public override Auth.OAuthRefreshResult refresh(Map authProviderConfiguration, String refreshToken){ 218 | try{ 219 | // Check if the "per user" principal is enabled and if so set the logged in user Id 220 | // This way the correct user is 221 | if(Boolean.valueOf(authProviderConfiguration.get(ENABLE_PER_USER_MODE_FIELD_NAME))){ 222 | loggedInUserId = refreshToken; 223 | } 224 | 225 | // Retrieve a new token from the token endpoint 226 | TokenResponse tokenResponse = this.retrieveToken(authProviderConfiguration); 227 | 228 | // Manage the login history 229 | this.handleInsertHistoryRecord( 230 | authProviderConfiguration, 231 | 'Refresh', 232 | String.isNotBlank(tokenResponse.access_token), 233 | 'JWT_CLIENT_CREDENTIALS', 234 | this.tokenCallout 235 | ); 236 | 237 | 238 | // Check if the per user principal is enabled, if so update the log entry for that user mapping 239 | if( Boolean.valueOf(authProviderConfiguration.get(ENABLE_PER_USER_MODE_FIELD_NAME)) && 240 | Boolean.valueOf(authProviderConfiguration.get(ENABLE_LOGIN_LOGGING_FIELD_NAME))){ 241 | 242 | // Update the mapping record 243 | getAuthProviderUtil().call('updateMappingLoginDetails', new Map { 244 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 245 | 'userId' => loggedInUserId 246 | }); 247 | } 248 | 249 | // Error with the token, purposely thrown here to manage proper error logging 250 | if(String.isBlank(tokenResponse.access_token)){ 251 | throw new TokenException(String.format(INVALID_TOKEN_RESPONSE_MSG, new String[]{this.tokenCallout.getResponse().getBody()})); 252 | } 253 | 254 | // Return the (refresh) token response 255 | return new Auth.OAuthRefreshResult( 256 | tokenResponse.access_token, 257 | refreshToken 258 | ); 259 | }catch(Exception e){ 260 | handleException(e, authProviderConfiguration); 261 | } 262 | 263 | // Unreachable statement to please the apex compiler 264 | return null; 265 | } 266 | 267 | 268 | /** 269 | * @description Returns information from the custom authentication provider about the current user. 270 | * This information is used by the registration handler and in other authentication 271 | * provider flows. 272 | * @param authProviderConfiguration The configuration items for the custom authentication 273 | * provider that have been configured in the custom metadata 274 | * type. 275 | * @param response The OAuth access token, OAuth secret or refresh token, 276 | * and state provided by the authentication provider to 277 | * authenticate the current user. 278 | * @return Creates a new instance of the Auth.UserData class. 279 | * @note User data is not being used in the OAUth 2.0 JWT Client Authentication flow as it 280 | * is a system to system integration. As some basic required info I put in an 281 | * integration user. 282 | */ 283 | public Auth.UserData getUserInfo(Map authProviderConfiguration, Auth.AuthProviderTokenResponse response) { 284 | return this.loggedInUserData; 285 | } 286 | 287 | 288 | /** 289 | * @description Returns the custom metadata type API name for a custom OAuth-based authentication 290 | * provider for single sign-on to Salesforce. 291 | * @return The custom metadata type API name for the authentication provider. 292 | */ 293 | public String getCustomMetadataType() { 294 | return String.valueOf(OAuth_JWT_Client_Authentication__mdt.getSObjectType()); 295 | } 296 | 297 | 298 | /** **************************************************************************************************** ** 299 | ** PRIVATE SUPPORT METHODS ** 300 | ** **************************************************************************************************** **/ 301 | /** 302 | * @description Method to Generate the standard Salesforce Auth Provider callback URL for the 303 | * specific Auth Provider Name. 304 | * @param authProviderConfiguration The configuration items for the custom authentication 305 | * provider that have been configured in the custom 306 | * metadata type. 307 | * @return The Auth Provider's callback URL 308 | * @false-positives The URL is generated at a known source and no danger. It's not user updateable. 309 | * Also the remote site settings will prevent any unauthorised endpoint call-outs 310 | */ 311 | @SuppressWarnings('PMD.ApexOpenRedirect') 312 | private PageReference getSfdcCallbackURL(Map authProviderConfiguration){ 313 | 314 | // If you have custom callback URL specified, return the custom callback URL 315 | if(String.isNotBlank(authProviderConfiguration.get(CUSTOM_CALLBACK_FIELD_NAME)?.trim())){ 316 | return new PageReference(authProviderConfiguration.get(CUSTOM_CALLBACK_FIELD_NAME)?.trim()); 317 | } 318 | 319 | // By default generate the Standard Salesforce Callback URL for the Auth Provider 320 | return new PageReference( 321 | URL.getOrgDomainUrl().toExternalForm() + '/services/authcallback/' + authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim() 322 | ); 323 | } 324 | 325 | 326 | /** 327 | * @description Method to parse the token response JSON into a TokenResponse Object 328 | * @param tokenResponseJSON The JSON response returned from the Authorisation Server 329 | * @return A TokenResponse Object 330 | */ 331 | private TokenResponse parseTokenResponse(String tokenResponseJSON){ 332 | return (TokenResponse) JSON.deserialize(tokenResponseJSON, TokenResponse.class); 333 | } 334 | 335 | 336 | 337 | 338 | /** 339 | * @description Method that generates the JWT, JWS and HTTP Request to retrieve an access token 340 | * from the configured token endpoint. 341 | * @param authProviderConfiguration The configuration items for the custom authentication 342 | * provider that have been configured in the custom metadata 343 | * type. 344 | * @return A TokenResponse with the access token 345 | * @throws TokenException There is an error in parsing the token response 346 | */ 347 | @TestVisible 348 | private TokenResponse retrieveToken(Map authProviderConfiguration){ 349 | 350 | // Call SF token endpoint 351 | this.tokenCallout = new utl.Rst(); 352 | 353 | // Generate the full body string as a URL query parameter 354 | Map bodyParameters = new Map{ 355 | 'grant_type' => 'client_credentials', 356 | 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 357 | 'client_assertion' => (Test.isRunningTest()) ? TEST_ASSERTION : this.generateJWS(authProviderConfiguration), 358 | 'scope' => authProviderConfiguration.get(SCOPE_FIELD_NAME)?.trim() 359 | }; 360 | 361 | // Add optional custom headers to the token request (i.e. things like API keys, X-Correlation-Id etc.) 362 | this.addStringHeadersToRequest( 363 | authProviderConfiguration.get(TOKEN_HEADERS_FIELD_NAME)?.trim(), 364 | this.tokenCallout 365 | ); 366 | 367 | // Add optional custom parameters to the token request (i.e. things client_id , tentant etc.) 368 | this.addStringParametersToRequest( 369 | authProviderConfiguration.get(TOKEN_PARAMS_FIELD_NAME)?.trim(), 370 | bodyParameters 371 | ); 372 | 373 | // Configure and call the endpoint 374 | this.tokenCallout.setEndpoint(authProviderConfiguration.get(TOKEN_ENDPOINT_FIELD_NAME)); 375 | this.tokenCallout.setContentTypeHeaderToFormUrlEncoded(bodyParameters); 376 | this.tokenCallout.call(); 377 | 378 | // Parse the response into a token response 379 | return this.parseTokenResponse(this.tokenCallout.getResponse().getBody()); 380 | } 381 | 382 | 383 | /** 384 | * @description Method to generate a JWT and JWS Compact Serialization 385 | * This is in a custom method because the Salesforce Auth.JWS Class does not allow you 386 | * to change to any other algorithms than SHA256 (unless I missed something 387 | * somewhere...) 388 | * @param authProviderConfiguration The configuration items for the custom authentication 389 | * provider that have been configured in the custom metadata 390 | * type. 391 | * @return JWS Compact Serialized, generated from the configuration 392 | */ 393 | @TestVisible 394 | private String generateJWS(Map authProviderConfiguration){ 395 | 396 | // Create the JWK header 397 | String header = JSON.serialize(new Map{ 398 | 'alg' => this.validateJwsHeaderAlgorithm(authProviderConfiguration.get(JWT_ALGORITHM_FIELD_NAME)?.trim()), 399 | 'typ' => 'JWT', 400 | 'kid' => authProviderConfiguration.get(JWT_KID_FIELD_NAME)?.trim() 401 | }); 402 | 403 | // Create the JWT payload 404 | String payload = JSON.serialize(new Map{ 405 | 'iss' => authProviderConfiguration.get(JWT_ISSUER_FIELD_NAME)?.trim(), 406 | 'aud' => authProviderConfiguration.get(JWT_AUDIENCE_FIELD_NAME)?.trim(), 407 | 'sub' => getSubject(authProviderConfiguration), 408 | 'exp' => (DateTime.now().addSeconds(300).getTime() / 1000), 409 | 'jti' => GUID 410 | }); 411 | 412 | // Encode and combine the header and body for signing 413 | String b64UrlEncodedHeaderAndPayload = String.format('{0}.{1}', 414 | new String[]{ 415 | base64UrlEncode(Blob.valueOf(header )), 416 | base64UrlEncode(Blob.valueOf(payload)) 417 | } 418 | ); 419 | 420 | // Generate the signature 421 | String b64UrlEncodedSignature = (!Test.isRunningTest()) ? this.base64UrlEncode( 422 | Crypto.signWithCertificate( 423 | this.validateJwsSigningAlgorithm(authProviderConfiguration.get(JWS_SIGNING_ALGORITHM_FIELD_NAME)?.trim()), 424 | Blob.valueOf(b64UrlEncodedHeaderAndPayload), 425 | authProviderConfiguration.get(JWS_SIGNING_CERT_FIELD_NAME)?.trim() 426 | ) 427 | ) : '[TEST_CLASS_VALUE_BECAUSE_THERE_IS_NO_MOCK_CERT_OPTION]'; 428 | 429 | // Create and return the JWT in a signed and compact serialization 430 | return String.format('{0}.{1}', new String[]{ 431 | b64UrlEncodedHeaderAndPayload, 432 | b64UrlEncodedSignature 433 | }); 434 | } 435 | 436 | 437 | /** **************************************************************************************************** ** 438 | ** PRIVATE UTILITY METHODS ** 439 | ** **************************************************************************************************** **/ 440 | /** 441 | * @description Method for encoding a Blob into a Base64 URL encoded String. 442 | * This is required for generating the JWS 443 | * @param input The input Blob to convert to a Base64Url Encoded String 444 | * @return Base64 Url Encoded String 445 | */ 446 | private String base64UrlEncode(Blob input){ 447 | return EncodingUtil.base64Encode(input).replace('+', '-').replace('/', '_'); 448 | } 449 | 450 | 451 | /** 452 | * @description Methods to add headers from a comma separated key/value pair string to an HttpRequest Object. 453 | * Example: "apiKey : apiValue, sysId : sysValue" wil add two header values. 454 | * This is required in order to add additional headers to the token response from 455 | * custom metadata in the auth provider. Only text fields are supported. Not long text 456 | * fields. This is a simple fix. 457 | * @param headerValue The comma separated key/value pairs of headers to add 458 | * @param httpRequest The HttpRequest object to add the header values to 459 | */ 460 | private void addStringHeadersToRequest(String headerValue, utl.Rst callout){ 461 | 462 | // Validate the header has a value 463 | if(String.isBlank(headerValue)){ 464 | return; 465 | } 466 | 467 | // Each header should be split by a comma, due to fact multi line is not supported 468 | for(String line : headerValue.split(',')){ 469 | 470 | // Check the line contains a colon but still has a value after the colon 471 | if(line.contains(':')){ 472 | 473 | // Split the value in key value pair 474 | String[] keyValueList = line.split(':'); 475 | 476 | // Validate the header is correct and has a 477 | if(keyValueList.size() == 2){ 478 | callout.setHeader(keyValueList[0]?.trim(), keyValueList[1]?.trim()); 479 | } 480 | } 481 | } 482 | } 483 | 484 | 485 | /** 486 | * @description Method to add additional URL encoded POST Body values to the body. Will take the 487 | * base string followed by an ampersand and the value of the parameters. 488 | * Parameter format is a colon separated key value pair. For multiple values split the 489 | * key value pairs with a comma 490 | * @param parameterValue The comma separated key/value pairs of parameters to add. i.e. 491 | * client_id : [CLIENT_ID], tenant : abcde 492 | * @param httpRequest The HttpRequest object to add the parameter values to 493 | */ 494 | private void addStringParametersToRequest(String parameterValue, Map parameterMap){ 495 | 496 | // Validate the parameter has a value 497 | if(String.isBlank(parameterValue)){ 498 | return; 499 | } 500 | 501 | // Each header should be split by a comma, due to fact multi line is not supported 502 | for(String line : parameterValue.split(',')){ 503 | 504 | // Check the line contains a colon but still has a value after the colon 505 | if(line.contains(':')){ 506 | // Split the value in key value pair 507 | String[] keyValueList = line.split(':'); 508 | 509 | // Validate the header is correct and has a 510 | if(keyValueList.size() == 2){ 511 | parameterMap.put(keyValueList[0]?.trim(), keyValueList[1]?.trim()); 512 | } 513 | } 514 | } 515 | } 516 | 517 | 518 | /** 519 | * @description Method to validate an Apex Supported JWS Header Algorithm 520 | * @param algorithm The name of the algorithm to validate 521 | * @return Name of the input algorithm if validated 522 | * @throws JwsException The algorithm is invalid 523 | */ 524 | @TestVisible 525 | private String validateJwsHeaderAlgorithm(String algorithm){ 526 | if(String.isNotBlank(algorithm)){ 527 | if(VALID_JWS_HEADER_ALGORITHMS.contains(algorithm?.trim())){ 528 | return algorithm; 529 | } 530 | } 531 | throw new JwsException(JWS_INVALID_HEAD_ALG_EXCEPTION_MSG); 532 | } 533 | 534 | 535 | /** 536 | * @description Method to validate an Apex Supported JWS Signing Algorithm 537 | * @param algorithm The name of the algorithm to validate 538 | * @return Name of the input algorithm if validated 539 | * @throws JwsException The algorithm is invalid 540 | */ 541 | @TestVisible 542 | private String validateJwsSigningAlgorithm(String algorithm){ 543 | if(String.isNotBlank(algorithm)){ 544 | if(VALID_JWS_SIGNING_ALGORITHMS.contains(algorithm?.trim())){ 545 | return algorithm; 546 | } 547 | } 548 | throw new JwsException(JWS_INVALID_SIGN_ALG_EXCEPTION_MSG); 549 | } 550 | 551 | 552 | /** 553 | * @description Method that logs an exception and transforms any exception type into a 554 | * GenericException 555 | * @param e The exception that is thrown and needs to be handled 556 | * @param authProviderConfiguration The configuration items for the custom authentication 557 | * provider that have been configured in the custom metadata 558 | * type. 559 | * @throws GenericException Always 560 | */ 561 | @TestVisible 562 | private static void handleException(Exception e, Map authProviderConfiguration){ 563 | 564 | // Generate a generic exception message for the error handling and to be thrown to the user 565 | String exceptionMessage = String.format(GENERIC_EXCEPTION_MSG, new String[]{e.getTypeName(), e.getMessage()}); 566 | 567 | // If logging is enabled use the "Lightweight - Auth Provider Util v2" method to insert an error log 568 | // This does mean a dependency with a different package, simply remove this code if you don't 569 | // want to install the dependency 570 | if(Boolean.valueOf(authProviderConfiguration.get(ENABLE_ERROR_LOGGING_FIELD_NAME))){ 571 | getAuthProviderUtil().call('insertLog', new Map { 572 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 573 | 'userId' => UserInfo.getUserId(), 574 | 'logId' => GUID, 575 | 'message' => exceptionMessage 576 | }); 577 | } 578 | 579 | // Throw the new generic exception to the user# 580 | throw new GenericException(exceptionMessage); 581 | } 582 | 583 | 584 | /** 585 | * @description Method to Switch the mapped subject between per user principal and Named Principal Mode 586 | * as a Identity Type 587 | * If per user principal is enabled use the "Lightweight - Auth Provider Util v2" method 588 | * and populate the mapping fields for each user that is allowed to get a token. 589 | * @param authProviderConfiguration The configuration items for the custom authentication 590 | * provider that have been configured in the custom metadata 591 | * type. 592 | * @return The subject that will be part of the JWT sub parameter 593 | */ 594 | private static String getSubject(Map authProviderConfiguration){ 595 | 596 | // Check if the per user principal is enabled 597 | if(Boolean.valueOf(authProviderConfiguration.get(ENABLE_PER_USER_MODE_FIELD_NAME))){ 598 | 599 | 600 | // If no user mapping exists throw an error 601 | if(! (Boolean) getAuthProviderUtil().call('checkUserMappingExists', new Map { 602 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 603 | 'userId' => UserInfo.getUserId() 604 | })){ 605 | throw new SubjectException( 606 | String.format( 607 | NO_USER_MAPPING_MSG, 608 | new String[]{authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME) , UserInfo.getUserId()} 609 | ) 610 | ); 611 | } 612 | 613 | // Return the subject from the user mapping record related to this user and auth provider 614 | return (String) getAuthProviderUtil().call('getSubjectFromUserMapping', new Map { 615 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 616 | 'userId' => UserInfo.getUserId() 617 | }); 618 | } 619 | 620 | // By default return the NamedPrincipal 621 | return authProviderConfiguration.get(JWT_SUBJECT_FIELD_NAME)?.trim(); 622 | } 623 | 624 | 625 | /** 626 | * @description Method to set the user id for the logged in user 627 | * @param callbackState The callback state containing the the cookie headers 628 | */ 629 | @TestVisible 630 | private void setLoggedInUserDetails(Auth.AuthProviderCallbackState callbackState){ 631 | 632 | // Get the data for the logged in user based on the cookie header 633 | loggedInUserData = (Auth.UserData) getAuthProviderUtil().call('getAuthUserDataFromCookieHeader', new Map { 634 | 'cookieHeader' => (!Test.IsRunningTest()) ? callbackState.headers.get('Cookie') : TEST_COOKIE_HEADER 635 | }); 636 | // Extract the user Id for ease of use later 637 | loggedInUserId = loggedInUserData.identifier; 638 | } 639 | 640 | 641 | /** 642 | * @description Method to get an instance of the AuthProviderUtil class. 643 | * This option requires the "Lightweight - Auth Provider Util v2" (04t4K000002Jv1tQAC) 644 | * package to be installed 645 | * @return Instance of the AuthProviderUtil class 646 | * @throws GenericException The lwt.AuthProviderUtil class does not exist. 647 | */ 648 | @TestVisible 649 | private static Callable getAuthProviderUtil(){ 650 | 651 | // Implementation in your normal code to return the Mock callable during an Apex Unit Test 652 | if(Test.isRunningTest()){ 653 | return (Callable) utl.Clbl.getInstance(); 654 | } 655 | 656 | // Lazy loading 657 | if(authProviderUtil == null){ 658 | 659 | // Dymaically instatiate class 660 | authProviderUtil = (Callable) Type.forName('lwt','AuthProviderUtil')?.newInstance(); 661 | 662 | // Throw an error if the package is not installed 663 | // Add Test check here so the test does not fail in case the package is installed 664 | if(authProviderUtil == null){ 665 | throw new GenericException(MISSING_UTIL_PACKAGE_MSG); 666 | } 667 | } 668 | return authProviderUtil; 669 | } 670 | 671 | /** 672 | * @description Method that generates the JWT, JWS and HTTP Request to retrieve an access token 673 | * from the configured token endpoint. 674 | * @param authProviderConfiguration The configuration items for the custom authentication 675 | * provider that have been configured in the custom metadata 676 | * type. 677 | * @param flowType The type of login flow (Initial or Refresh) 678 | * @param success Inidcator if the login was successful or not 679 | * @param providerType Optional description of the provider. This is handy when you 680 | * have chained callouts. 681 | * @param callout The callout details for error logging 682 | */ 683 | @SuppressWarnings('PMD.ExcessiveParameterList') 684 | private void handleInsertHistoryRecord(Map authProviderConfiguration, String flowType, Boolean success, String providerType, utl.Rst callout){ 685 | // If login history is enabled, create a login history entry 686 | if(Boolean.valueOf(authProviderConfiguration.get(ENABLE_LOGIN_HISTORY_FIELD_NAME))){ 687 | 688 | System.debug('INSERTING history as ' + UserInfo.getUserName() + ' - '+ UserInfo.getLastName()); 689 | 690 | getAuthProviderUtil().call('insertLoginHistoryRecord', new Map { 691 | 'authProviderName' => authProviderConfiguration.get(AUTH_PROVIDER_NAME_FIELD_NAME)?.trim(), 692 | 'userId' => loggedInUserId, 693 | 'flowType' => flowType, 694 | 'timestamp' => Datetime.now(), 695 | 'success' => success, 696 | 'providerType' => providerType, 697 | 'loginInfo' => success ? null : callout?.getResponse()?.getBody() 698 | }); 699 | } 700 | } 701 | 702 | 703 | /** **************************************************************************************************** ** 704 | ** PRIVATE EXCEPTION CLASSES ** 705 | ** **************************************************************************************************** **/ 706 | /** 707 | * @description Custom Exception thrown when there is an issue generating the JWS. 708 | */ 709 | @TestVisible 710 | private class JwsException extends Exception{} 711 | 712 | 713 | /** 714 | * @description Custom Exception thrown when there is an issue generating the token. 715 | */ 716 | @TestVisible 717 | private class TokenException extends Exception{} 718 | 719 | 720 | /** 721 | * @description Custom Exception thrown when there is an issue related the subject 722 | */ 723 | @TestVisible 724 | private class SubjectException extends Exception{} 725 | 726 | 727 | /** 728 | * @description Custom Exception thrown when there is an issue generating the token. 729 | */ 730 | @TestVisible 731 | private class GenericException extends Exception{} 732 | 733 | 734 | /** **************************************************************************************************** ** 735 | ** PRIVATE DATA STRUCTURE CLASSES ** 736 | ** **************************************************************************************************** **/ 737 | /** 738 | * @description Class representing the data strcuture of an OAuth Token Response as described in standard: 739 | * https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2 740 | * @false-positives Namings conventions have to match the JSON response format in order to properly 741 | * deserialize. In this case the naming conventions will not follow standard 742 | * conventions to accomodate this 743 | */ 744 | @SuppressWarnings('PMD.VariableNamingConventions, PMD.FieldNamingConventions') 745 | @TestVisible 746 | private class TokenResponse{ 747 | 748 | // Required 749 | public String access_token; 750 | public String token_type; 751 | 752 | // Optional 753 | public String expires_in; 754 | public String scope; 755 | public String state; 756 | } 757 | } -------------------------------------------------------------------------------- /force-app/package/classes/OAuthJwtClientCredentials.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/package/classes/OAuthJwtClientCredentialsTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Justus van den Berg (jfwberg@gmail.com) 3 | * @date April 2024 4 | * @copyright (c) 2023 Justus van den Berg 5 | * @license MIT (See LICENSE file in the project root) 6 | * @description Class for testing the OAuthJwtClientAuthentication Methods 7 | */ 8 | @IsTest 9 | private with sharing class OAuthJwtClientCredentialsTest { 10 | 11 | /** **************************************************************************************************** ** 12 | ** PRIVATE TEST DATA VARIABLE ** 13 | ** **************************************************************************************************** **/ 14 | // Variables for holding test data 15 | private static OAuthJwtClientCredentials authProvider; 16 | private static Map authProviderConfig; 17 | 18 | 19 | /** **************************************************************************************************** ** 20 | ** TEST METHODS ** 21 | ** **************************************************************************************************** **/ 22 | /** 23 | * @description Method to test the initiate function 24 | */ 25 | @IsTest 26 | static void testInitiate(){ 27 | 28 | // Test the method 29 | Test.startTest(); 30 | PageReference pageReference = getAuthProvider().initiate(getAuthProviderConfig(),'TestState'); 31 | Test.stopTest(); 32 | 33 | // Assert test results 34 | Assert.areEqual( 35 | pageReference.getUrl(), 36 | URL.getOrgDomainUrl().toExternalForm() + '/services/authcallback/TestAuthProvider?state=TestState', 37 | 'Unexpected callback URL' 38 | ); 39 | } 40 | 41 | 42 | /** 43 | * @description Method to test the handleCallback() function 44 | */ 45 | @IsTest 46 | static void testHandleCallback(){ 47 | 48 | // Set a mock resonse for the token 49 | Test.setMock(HttpCalloutMock.class, utl.Mck.getInstance()); 50 | setTokenResponseMock(); 51 | 52 | // Test the method 53 | Test.startTest(); 54 | Auth.AuthProviderTokenResponse tokenResponse = getTokenResponse(); 55 | getAuthProvider().generateJWS(getAuthProviderConfig()); 56 | Test.stopTest(); 57 | 58 | // Assert test results 59 | Assert.areEqual('access_token_value', tokenResponse.oauthToken, 'Unexpected oauthToken value'); 60 | Assert.areEqual('TestState', tokenResponse.state , 'Unexpected state value'); 61 | } 62 | 63 | 64 | /** 65 | * @description Method to test the refresh() function 66 | */ 67 | @IsTest 68 | static void testRefresh(){ 69 | 70 | // Set a mock resonse for the token 71 | Test.setMock(HttpCalloutMock.class, utl.Mck.getInstance()); 72 | setTokenResponseMock(); 73 | 74 | // Test the method 75 | Test.startTest(); 76 | Auth.OAuthRefreshResult refreshResult = getAuthProvider().refresh(getAuthProviderConfig(),'[REFRESH_TOKEN]'); 77 | Test.stopTest(); 78 | 79 | // Assert test results 80 | Assert.areEqual('access_token_value', refreshResult.accessToken, 'Unexpected accessToken value'); 81 | } 82 | 83 | 84 | /** 85 | * @description Method to test the refresh() function 86 | */ 87 | @IsTest 88 | static void testPerUser(){ 89 | 90 | // Enable per user mode, error loging and login history logging. 91 | getAuthProviderConfig().put(OAuthJwtClientCredentials.ENABLE_PER_USER_MODE_FIELD_NAME,'true'); 92 | getAuthProviderConfig().put(OAuthJwtClientCredentials.ENABLE_ERROR_LOGGING_FIELD_NAME,'true'); 93 | getAuthProviderConfig().put(OAuthJwtClientCredentials.ENABLE_LOGIN_LOGGING_FIELD_NAME,'true'); 94 | getAuthProviderConfig().put(OAuthJwtClientCredentials.ENABLE_LOGIN_HISTORY_FIELD_NAME,'true'); 95 | 96 | // Set action responses 97 | utl.Clbl.setActionResponse('insertLoginHistoryRecord', null); 98 | utl.Clbl.setActionResponse('updateMappingLoginDetails', null); 99 | utl.Clbl.setActionResponse('insertLog', null); 100 | utl.Clbl.setActionResponse('handleInsertHistoryRecord', null); 101 | utl.Clbl.setActionResponse('checkUserMappingExists', true); 102 | utl.Clbl.setActionResponse('getSubjectFromUserMapping', UserInfo.getUserName()); 103 | utl.Clbl.setActionResponse('getAuthUserDataFromCookieHeader',getAuthProvider().loggedInUserData); 104 | 105 | // Set a mock resonse for the token 106 | Test.setMock(HttpCalloutMock.class, utl.Mck.getInstance()); 107 | setTokenResponseMock(); 108 | 109 | // Test the method 110 | Test.startTest(); 111 | getAuthProvider().refresh(getAuthProviderConfig(),'[REFRESH_TOKEN]'); 112 | getTokenResponse(); 113 | getAuthProvider().generateJWS(getAuthProviderConfig()); 114 | Test.stopTest(); 115 | 116 | // Assert test results 117 | Assert.areEqual(true, true, 'Coverage only'); 118 | } 119 | 120 | 121 | /** 122 | * @description Method to test the getUserInfo() function 123 | */ 124 | @IsTest 125 | static void testGetUserInfo(){ 126 | 127 | // Set a mock resonse for the token 128 | Test.setMock(HttpCalloutMock.class, utl.Mck.getInstance()); 129 | setTokenResponseMock(); 130 | 131 | // Test the method 132 | Test.startTest(); 133 | Auth.UserData userData = getAuthProvider().getUserInfo( 134 | getAuthProviderConfig(), 135 | getTokenResponse() 136 | ); 137 | Test.stopTest(); 138 | 139 | // Assert test results 140 | Assert.areEqual(UserInfo.getUserEmail(), userData.email, 'Unexpected email value'); 141 | } 142 | 143 | 144 | /** 145 | * @description Method to test the getCustomMetadataType() function 146 | */ 147 | @IsTest 148 | static void testGetCustomMetadataType(){ 149 | // Assert test results 150 | Assert.areEqual( 151 | String.valueOf(OAuth_JWT_Client_Authentication__mdt.getSObjectType()), 152 | getAuthProvider().getCustomMetadataType(), 153 | 'Unexpected custom metadata value' 154 | ); 155 | } 156 | 157 | 158 | /** 159 | * @description Method to test the generateHttpRequest() function 160 | */ 161 | @IsTest 162 | static void testGenerateHttpRequest(){ 163 | 164 | // Setup the mock response 165 | utl.Mck.setResponse(200, '{"SUCCESS": true}'); 166 | 167 | // Set the mock response 168 | Test.setMock(HttpCalloutMock.class, utl.Mck.getInstance()); 169 | 170 | // Test the http request is generated correcty 171 | Test.startTest(); 172 | getAuthProvider().retrieveToken(getAuthProviderConfig()); 173 | Test.stopTest(); 174 | 175 | // Validate request body 176 | Assert.areEqual( 177 | 'grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=%5BTEST_ASSERTION%5D&scope=web%2Capi&tenant=%5BTENANT%5D&client_id=%5BCLIENT_ID%5D', 178 | getAuthProvider().tokenCallout.getRequest().getBody(), 179 | 'Unexpected request body' 180 | ); 181 | 182 | // Assert test results for custom headers 183 | Assert.areEqual('[TEST_KEY]', getAuthProvider().tokenCallout.getRequest().getHeader('apiKey'),'Expected header "apiKey" does not exist'); 184 | Assert.areEqual('[API_ID]', getAuthProvider().tokenCallout.getRequest().getHeader('apiId'), 'Expected header "apiId" does not exist'); 185 | 186 | // assert result for the endpoint 187 | Assert.areEqual( 188 | getAuthProviderConfig().get(OAuthJwtClientCredentials.TOKEN_ENDPOINT_FIELD_NAME), 189 | getAuthProvider().tokenCallout.getRequest().getEndpoint(), 190 | 'Unexpected endpoint' 191 | ); 192 | } 193 | 194 | 195 | /** 196 | * @description Method to test the validateJwsHeaderAlgorithm() function 197 | */ 198 | @IsTest 199 | static void testValidateJwsHeaderAlgorithm(){ 200 | // Happy path 201 | Assert.areEqual('RS512', getAuthProvider().validateJwsHeaderAlgorithm('RS512'), 'Unexpected algorithm'); 202 | 203 | // Exception path 204 | try { 205 | getAuthProvider().validateJwsHeaderAlgorithm('invalid'); 206 | }catch(OAuthJwtClientCredentials.JwsException e){ 207 | utl.Tst.assertExceptionMessage(OAuthJwtClientCredentials.JWS_INVALID_HEAD_ALG_EXCEPTION_MSG, e); 208 | } 209 | } 210 | 211 | 212 | /** 213 | * @description Method to test the validateJwsHeaderAlgorithm() function 214 | */ 215 | @IsTest 216 | static void testValidateJwsSigningAlgorithm(){ 217 | // Happy path 218 | Assert.areEqual('RSA-SHA512', getAuthProvider().validateJwsSigningAlgorithm('RSA-SHA512'), 'Unexpected algorithm'); 219 | 220 | // Exception path 221 | try { 222 | getAuthProvider().validateJwsSigningAlgorithm('invalid'); 223 | }catch(OAuthJwtClientCredentials.JwsException e){ 224 | utl.Tst.assertExceptionMessage(OAuthJwtClientCredentials.JWS_INVALID_SIGN_ALG_EXCEPTION_MSG, e); 225 | } 226 | } 227 | 228 | 229 | /** 230 | * @description Method to test the exceptions thrown in case there is no package installed 231 | */ 232 | @IsTest 233 | static void testGetAuthProviderUtil(){ 234 | try{ 235 | OAuthJwtClientCredentials.getAuthProviderUtil(); 236 | }catch(Exception e){ 237 | try{ 238 | OAuthJwtClientCredentials.handleException(e,getAuthProviderConfig()); 239 | }catch(Exception se){ 240 | utl.Tst.assertExceptionMessage( 241 | OAuthJwtClientCredentials.GENERIC_EXCEPTION_MSG, 242 | e.getTypeName(), 243 | e.getMessage(), 244 | se 245 | ); 246 | } 247 | } 248 | } 249 | 250 | 251 | /** **************************************************************************************************** ** 252 | ** PRIVATE TEST DATA METHODS ** 253 | ** **************************************************************************************************** **/ 254 | /** 255 | * @description Method for setting up a 256 | */ 257 | private static void setTokenResponseMock(){ 258 | 259 | OAuthJwtClientCredentials.TokenResponse tokenResponse = new OAuthJwtClientCredentials.TokenResponse(); 260 | tokenResponse.access_token = 'access_token_value'; 261 | tokenResponse.token_type = 'Bearer'; 262 | tokenResponse.expires_in = '1682439225'; 263 | 264 | utl.Mck.setResponse(200, JSON.serializePretty(tokenResponse)); 265 | } 266 | 267 | 268 | /** 269 | * @description Method that return a mock token response 270 | * @return Mock token reponse 271 | */ 272 | private static Auth.AuthProviderTokenResponse getTokenResponse(){ 273 | return getAuthProvider().handleCallback( 274 | getAuthProviderConfig(), 275 | new Auth.AuthProviderCallbackState( 276 | null, 277 | null, 278 | new Map{ 279 | 'code' => 'NoCodeRequiredButMandatory', 280 | 'state'=> 'TestState' 281 | } 282 | ) 283 | ); 284 | } 285 | 286 | 287 | /** 288 | * @description Method to create a Auth Provider (OAuthJwtClientCredentials) class instance that is 289 | * used for testing 290 | * @return Class representing the Auth Provider 291 | */ 292 | private static OAuthJwtClientCredentials getAuthProvider(){ 293 | if(authProvider == null){ 294 | authProvider = new OAuthJwtClientCredentials(); 295 | } 296 | return authProvider; 297 | } 298 | 299 | 300 | /** 301 | * @description Method to generate the Auth Provider Config data that is used for testing 302 | * @return The auth provider configuration data map 303 | */ 304 | private static Map getAuthProviderConfig(){ 305 | if(authProviderConfig == null){ 306 | authProviderConfig= new Map{ 307 | OAuthJwtClientCredentials.SCOPE_FIELD_NAME => 'web,api', 308 | OAuthJwtClientCredentials.TOKEN_ENDPOINT_FIELD_NAME => 'https://localhost/oauth/token', 309 | OAuthJwtClientCredentials.TOKEN_HEADERS_FIELD_NAME => 'apiKey : [TEST_KEY], apiId : [API_ID]', 310 | OAuthJwtClientCredentials.TOKEN_PARAMS_FIELD_NAME => 'tenant : [TENANT], client_id : [CLIENT_ID]', 311 | OAuthJwtClientCredentials.JWT_ALGORITHM_FIELD_NAME => 'RS512', 312 | OAuthJwtClientCredentials.JWT_KID_FIELD_NAME => 'TEST-KEY-ID', 313 | OAuthJwtClientCredentials.JWT_SUBJECT_FIELD_NAME => '[SUBJECT]', 314 | OAuthJwtClientCredentials.JWT_ISSUER_FIELD_NAME => '[ISSUER]', 315 | OAuthJwtClientCredentials.JWT_AUDIENCE_FIELD_NAME => '[AUDIENCE]', 316 | OAuthJwtClientCredentials.AUTH_PROVIDER_NAME_FIELD_NAME => 'TestAuthProvider', 317 | OAuthJwtClientCredentials.JWS_SIGNING_CERT_FIELD_NAME => 'certName', 318 | OAuthJwtClientCredentials.JWS_SIGNING_ALGORITHM_FIELD_NAME => 'RSA-SHA512', 319 | OAuthJwtClientCredentials.ENABLE_PER_USER_MODE_FIELD_NAME => 'false', 320 | OAuthJwtClientCredentials.ENABLE_ERROR_LOGGING_FIELD_NAME => 'false', 321 | OAuthJwtClientCredentials.ENABLE_LOGIN_LOGGING_FIELD_NAME => 'false', 322 | OAuthJwtClientCredentials.ENABLE_LOGIN_HISTORY_FIELD_NAME => 'false' 323 | }; 324 | } 325 | return authProviderConfig; 326 | } 327 | 328 | } -------------------------------------------------------------------------------- /force-app/package/classes/OAuthJwtClientCredentialsTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | -------------------------------------------------------------------------------- /force-app/package/layouts/OAuth_JWT_Client_Authentication__mdt-OAuth JWT Client Credential Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | true 7 | 8 | 9 | 10 | Required 11 | MasterLabel 12 | 13 | 14 | Required 15 | DeveloperName 16 | 17 | 18 | Required 19 | Auth_Provider_Name__c 20 | 21 | 22 | Required 23 | JWT_Signing_Certificate_Name__c 24 | 25 | 26 | Required 27 | Token_Endpoint_URL__c 28 | 29 | 30 | Required 31 | JWT_Subject__c 32 | 33 | 34 | Required 35 | JWT_Issuer__c 36 | 37 | 38 | Required 39 | JWT_Audience__c 40 | 41 | 42 | Required 43 | JWT_Algorithm__c 44 | 45 | 46 | Edit 47 | JWT_Kid__c 48 | 49 | 50 | Edit 51 | Additional_Token_Endpoint_Headers__c 52 | 53 | 54 | Required 55 | JWT_Signing_Algorithm__c 56 | 57 | 58 | Edit 59 | Scope__c 60 | 61 | 62 | Edit 63 | Custom_Callback_URL__c 64 | 65 | 66 | Edit 67 | Additional_Token_Endpoint_Parameters__c 68 | 69 | 70 | 71 | 72 | Edit 73 | IsProtected 74 | 75 | 76 | Required 77 | NamespacePrefix 78 | 79 | 80 | 81 | 82 | 83 | true 84 | true 85 | true 86 | 87 | 88 | 89 | Edit 90 | Enable_Error_Logging__c 91 | 92 | 93 | Edit 94 | Enable_Per_User_Mode__c 95 | 96 | 97 | Edit 98 | Enable_Per_User_Login_Logging__c 99 | 100 | 101 | 102 | 103 | 104 | 105 | false 106 | true 107 | true 108 | 109 | 110 | 111 | Readonly 112 | CreatedById 113 | 114 | 115 | 116 | 117 | Readonly 118 | LastModifiedById 119 | 120 | 121 | 122 | 123 | 124 | true 125 | false 126 | false 127 | 128 | 129 | 130 | 131 | 132 | 133 | false 134 | false 135 | false 136 | false 137 | false 138 | 139 | 00h0C000002YeFH 140 | 4 141 | 0 142 | Default 143 | 144 | 145 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/OAuth_JWT_Client_Authentication__mdt.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OAuth JWT Client Authentication 5 | Public 6 | 7 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Additional_Token_Endpoint_Headers__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Additional_Token_Endpoint_Headers__c 4 | false 5 | DeveloperControlled 6 | Addtional headers to add to the token endpoint. 7 | Each value pair needs to be comma separated and and split by a colon. . I.E. "apiKey : value, clientId : ab-cd-ef" 8 | 9 | 25000 10 | LongTextArea 11 | 3 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Additional_Token_Endpoint_Parameters__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Additional_Token_Endpoint_Parameters__c 4 | false 5 | DeveloperControlled 6 | Addtional params to add to the token endpoint in the POST body. 7 | Each value pair needs to be comma separated and and split by a colon. I.E. "tenant : ab-cd-ef, client_id : qr-st-uv" 8 | 9 | 25000 10 | LongTextArea 11 | 3 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Auth_Provider_Name__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Auth_Provider_Name__c 4 | false 5 | DeveloperControlled 6 | The API Name of the Auth Provider, this is used to generate the Salesforce Callback URL 7 | 8 | 255 9 | true 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Custom_Callback_URL__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom_Callback_URL__c 4 | false 5 | DeveloperControlled 6 | Use this to override the generation of the standard callback url for the AuthProvider. In most cases this will be blank. 7 | 8 | 255 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Enable_Error_Logging__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Enable_Error_Logging__c 4 | true 5 | false 6 | DeveloperControlled 7 | This option requires the "Lightweight - Auth Provider Util v2" (04t4K000002Jv1tQAC) package to be installed. 8 | 9 | Checkbox 10 | 11 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Enable_Login_History__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Enable_Login_History__c 4 | false 5 | false 6 | DeveloperControlled 7 | When Enabled any token request is logged. This requires the "Lightweight - Auth Provider Util" package to be installed. 8 | 9 | Checkbox 10 | 11 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Enable_Per_User_Login_Logging__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Enable_Per_User_Login_Logging__c 4 | false 5 | false 6 | DeveloperControlled 7 | This option requires the "Lightweight - Auth Provider Util v2" (04t4K000002Jv1tQAC) package to be installed. 8 | Enable this setting to enable logging on how often a token has been requested by a specific user. This can be helpful when just implementing this feature, the API log files will show the same info for longer term logging. 9 | 10 | Checkbox 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Enable_Per_User_Mode__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Enable_Per_User_Mode__c 4 | false 5 | false 6 | DeveloperControlled 7 | If per user mode is enabled a mapping needs to exist where each User Id is mapped to the external identifier. Use the "Auth Provider Util" App to create these mappings. If this is switched on the JWT Subject field will be populated based on the user mapping. 8 | This option requires the "Lightweight - Auth Provider Util v2" (04t4K000002Jv1tQAC) package to be installed. 9 | 10 | Checkbox 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Algorithm__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Algorithm__c 4 | 'RS256' 5 | false 6 | DeveloperControlled 7 | The algorithm used in the JWT i.e. RS256, RS512 8 | 9 | 255 10 | true 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Audience__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Audience__c 4 | false 5 | DeveloperControlled 6 | 7 | 255 8 | true 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Issuer__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Issuer__c 4 | false 5 | DeveloperControlled 6 | 7 | 255 8 | true 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Kid__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Kid__c 4 | false 5 | DeveloperControlled 6 | Optional Key Id for the JWT 7 | 8 | 255 9 | false 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Signing_Algorithm__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Signing_Algorithm__c 4 | 'RSA-SHA256' 5 | false 6 | DeveloperControlled 7 | The signing algorithm used by the Crypto class to sign the JWT. Defaults to 'RSA-SHA256'. This should match the algorithm of the JWT. The value names are just different, hence 2 fields. 8 | 9 | 255 10 | true 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Signing_Certificate_Name__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Signing_Certificate_Name__c 4 | false 5 | DeveloperControlled 6 | The API Name of the Certificate (stored in Salesforce) that is used to sign the JWT 7 | 8 | 80 9 | true 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/JWT_Subject__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JWT_Subject__c 4 | false 5 | DeveloperControlled 6 | 7 | 255 8 | true 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Scope__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Scope__c 4 | false 5 | DeveloperControlled 6 | 7 | 255 8 | false 9 | Text 10 | false 11 | 12 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/fields/Token_Endpoint_URL__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Token_Endpoint_URL__c 4 | false 5 | DeveloperControlled 6 | The URL of the the token endpoint 7 | 8 | 255 9 | true 10 | Text 11 | false 12 | 13 | -------------------------------------------------------------------------------- /force-app/package/objects/OAuth_JWT_Client_Authentication__mdt/validationRules/Auth_Pr_Name_Needs_To_Match_URL_Suffix.validationRule-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Auth_Pr_Name_Needs_To_Match_URL_Suffix 4 | true 5 | The "Auth Provider Name" value needs to match the record "URL Suffix" value 6 | NOT(Auth_Provider_Name__c == DeveloperName) 7 | The "Auth Provider Name" value needs to match the record "URL Suffix" value 8 | 9 | -------------------------------------------------------------------------------- /force-app/package/permissionsets/Lightweight_OAuth_JWT_Client_Credential_Auth_Provider.permissionset-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OAuthJwtClientCredentials 5 | true 6 | 7 | 8 | OAuthJwtClientCredentialsTest 9 | true 10 | 11 | false 12 | 13 | 14 | -------------------------------------------------------------------------------- /media/01_Auth_Provider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/01_Auth_Provider.png -------------------------------------------------------------------------------- /media/02_Auth_Provider_Setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/02_Auth_Provider_Setup.png -------------------------------------------------------------------------------- /media/03_Remote_Site_Setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/03_Remote_Site_Setting.png -------------------------------------------------------------------------------- /media/04_Permission_Set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/04_Permission_Set.png -------------------------------------------------------------------------------- /media/05_External_Credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/05_External_Credential.png -------------------------------------------------------------------------------- /media/06_Permission_Set_Mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/06_Permission_Set_Mapping.png -------------------------------------------------------------------------------- /media/07_Authenticate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/07_Authenticate.png -------------------------------------------------------------------------------- /media/08_Authentication_Success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/08_Authentication_Success.png -------------------------------------------------------------------------------- /media/09_Named_Credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/media/09_Named_Credential.png -------------------------------------------------------------------------------- /media/UserMapping.drawio: -------------------------------------------------------------------------------- 1 | 7VnbUtswEP2aPLbjSxLcRwgBpoUp07RT+qjYwlGRrYy8TuJ+fVeO5EtkIJkCZjrhIWiPVrK8Z/dItgf+JNlcSrJc3IiI8oHnRJuBfz7wPHcYBPhPIcUWCbxPWyCWLNJONTBjf6gGHY3mLKJZyxGE4MCWbTAUaUpDaGFESrFuu90L3r7qksTUAmYh4Tb6k0Ww0Hcxcmr8irJ4Ya7sOronIcZZA9mCRGLdgPzpwJ9IIWDbSjYTylXwTFy24y4e6a0WJmkK+wzwxnodUJiboxHeqzaFhIWIRUr4tEbPpMjTiKoZHLRqn2shlgi6CP6mAIUmjuQgEFpAwnUv3TC4U8M/jrT1q2Wdb/TcpVE0jFsqWUKBSoOlIIu7ptGYSZn1VKVl5rLjpEOXiVyGOhK+Ti0iY6q9dOKqGDWG6dheUoFrkwU6SMoJsFU7X4hOu7jyq5nBhibnEaJOjkQdQpTr9sZUcGTqEKYM9vZM6QuvCM/1pJ/zDPLM4g9VeqmaecJPQxAYqbMVlcBwR7gmc8pvRcaAiRRd5gJAJOjAVccZCR/iktuJ4Goczubfl3+NOU45i9VYUFw3SRU5cJbSSbWNPRlnNR3dPBlC3esN9fZT7NjrejfzNbRobGRj59+DfmIFfbrB5MNcR9TEf8zximdzia1YtTACeGvw3xMz8trEuN7bEWMOV/3olnuIalUKVYvSfhpVWruCt5dwBbZwnfSlW4FVQlXFmIJp1NRXGeOv4xofnL1ysytt+gOdzwkQKxnaVK8XDOhsScr4rPGY36b1BWrBHz9fCxX24sUwOhbD48VgDletA5fXVzmY1RxWD95+9fBj9l7roWvTfr168KwY3xD5QIGlKprfKUmOu3Yfxynz5uQoVF1C1fm8MexLqDoeOPYQKt8urC7puuRiXg6bUbliIbWfYvoWrMp+C8EyLDdirQNzlCvveV5eS6581wrvUa7qtyOOLVf9vR1xnlerK5zWKNVep6kv7+U0FfR4mPLtw9QHNG9RLtSHBytqqvMbzTMyZ5xB0e0wTSOVOULlStmMJFttT2eFyPE3zQHXfNE9+kqs1WhFZkYhX6o7LGdJSFquaYeuUihbnGQgxQM1QpeKVFXuPeN8ByJa7kLkjja11OhgwqKoLPuuJGinycvnwTCw86DKjWYeuMHhiYBm/WWn7Gt8H/OnfwE= -------------------------------------------------------------------------------- /scripts/00_security_scanner.bat: -------------------------------------------------------------------------------- 1 | rem SECURITY SCAN - PMD 2 | sfdx scanner:run -t "../force-app" -f html -o "scan-results/pmd-result.html" --verbose 3 | 4 | rem SECURITY SCAN - GRAPH RULES 5 | sfdx scanner:run:dfa --target "classes\*.cls" --projectdir "../force-app/package" -f html -o "scan-results/graph-result.html" --verbose -------------------------------------------------------------------------------- /scripts/01_package_dependencies.bat: -------------------------------------------------------------------------------- 1 | REM -------------------------------------------------------- 2 | REM MANGED DEPENDENCIES (PICK EITHER MANAGED OR UNLOCKED) - 3 | REM -------------------------------------------------------- 4 | rem Lightweight - Apex Unit Test Util v2@2.4.0-2 5 | sf package install -p "04tP3000000M6OXIA0" -w 30 6 | 7 | rem Lightweight - REST Util@0.11.0-1 8 | sf package install -p "04tP3000000M6gHIAS" -w 30 9 | 10 | REM ----------------- OPTIONAL BUT ADVICED ----------------- 11 | rem Lightweight - Auth Provider Util v2@0.12.0-1 12 | sf package install -p "04tP3000000MVUzIAO" -w 30 13 | 14 | 15 | REM -------------------------------------------------------- 16 | REM UNLOCKED DEPENDENCIES (PICK EITHER MANAGED OR UNLOCKED)- 17 | REM -------------------------------------------------------- 18 | rem Lightweight - Apex Unit Test Util v2 (Unlocked)@2.4.0-2 19 | sf package install -p "04tP3000000M6Q9IAK" -w 30 20 | 21 | rem Lightweight - REST Util (Unlocked)@0.11.0-1 22 | sf package install -p "04tP3000000M6htIAC" -w 30 23 | 24 | REM ----------------- OPTIONAL BUT ADVICED ----------------- 25 | rem Lightweight - Auth Provider Util v2 (Unlocked)@0.12.0-1 26 | sf package install -p "04tP3000000MW1FIAW" -w 30 27 | 28 | 29 | REM -------------------------------------------------------- 30 | REM ASSIGN PERMISSION SETS - 31 | REM -------------------------------------------------------- 32 | sf org assign permset --name "Lightweight_Apex_Unit_Test_Util_v2" 33 | sf org assign permset --name "Lightweight_REST_Util" 34 | sf org assign permset --name "Lightweight_Auth_Provider_Util" 35 | sf org assign permset --name "Lightweight_OAuth_JWT_Client_Credential_Auth_Provider" 36 | 37 | -------------------------------------------------------------------------------- /scripts/02_package_create_managed.bat: -------------------------------------------------------------------------------- 1 | REM ***************************** 2 | REM PACKAGE CREATION 3 | REM ***************************** 4 | 5 | REM Package Create Config 6 | SET devHub=devHubAlias 7 | SET packageName=Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider 8 | SET packageDescription=A lightweight generic Auth Provider Apex class that can be used with named/external credentials to get an access token using the OAuth 2.0 JWT Client Credentials Authentication Flow. 9 | SET packageType=Managed 10 | SET packagePath=force-app/package 11 | 12 | REM Package Config 13 | SET packageId=0Ho4K0000008OYeSAM 14 | SET packageVersionId=04tP3000000MWfZIAW 15 | 16 | REM Create package 17 | sf package create --name "%packageName%" --description "%packageDescription%" --package-type "%packageType%" --path "%packagePath%" --target-dev-hub %devHub% 18 | 19 | REM Create package version 20 | sf package version create --package "%packageName%" --target-dev-hub %devHub% --code-coverage --installation-key-bypass --wait 30 21 | 22 | REM Delete package 23 | sf package:delete -p %packageId% --target-dev-hub %devHub% --no-prompt 24 | 25 | REM Delete package version 26 | sf package:version:delete -p %packageVersionId% --target-dev-hub %devHub% --no-prompt 27 | 28 | REM Promote package version 29 | sf package:version:promote -p %packageVersionId% --target-dev-hub %devHub% --no-prompt 30 | -------------------------------------------------------------------------------- /scripts/03_package_create_unlocked.bat: -------------------------------------------------------------------------------- 1 | REM ***************************** 2 | REM PACKAGE CREATION 3 | REM ***************************** 4 | 5 | REM Package Create Config 6 | SET devHub=devHubAlias 7 | SET packageName=Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider (Unlocked) 8 | SET packageDescription=A lightweight generic Auth Provider Apex class that can be used with named/external credentials to get an access token using the OAuth 2.0 JWT Client Credentials Authentication Flow. 9 | SET packageType=Unlocked 10 | SET packagePath=force-app/package 11 | 12 | REM Package Config 13 | SET packageId=0HoP300000000ODKAY 14 | SET packageVersionId=04tP3000000MWndIAG 15 | 16 | REM Create package 17 | sf package create --name "%packageName%" --description "%packageDescription%" --package-type "%packageType%" --path "%packagePath%" --target-dev-hub %devHub% 18 | 19 | REM Create package version 20 | sf package version create --package "%packageName%" --target-dev-hub %devHub% --code-coverage --installation-key-bypass --wait 30 21 | 22 | REM Delete package 23 | sf package:delete -p %packageId% --target-dev-hub %devHub% --no-prompt 24 | 25 | REM Delete package version 26 | sf package:version:delete -p %packageVersionId% --target-dev-hub %devHub% --no-prompt 27 | 28 | REM Promote package version 29 | sf package:version:promote -p %packageVersionId% --target-dev-hub %devHub% --no-prompt 30 | -------------------------------------------------------------------------------- /scripts/04_package_test.bat: -------------------------------------------------------------------------------- 1 | REM ***************************** 2 | REM INSTALL ON TEST ORG 3 | REM ***************************** 4 | 5 | REM Config 6 | SET testOrg=orgAlias 7 | SET packageVersionId= 8 | SET dependencyVersionId= 9 | 10 | REM Install the package dependencies 11 | sf package:install -p %dependencyVersionId% --target-org %testOrg% --wait 30 12 | 13 | REM Install the package 14 | sf package:install -p %packageVersionId% --target-org %testOrg% --wait 30 15 | 16 | REM Uninstall the package 17 | sf package uninstall --package %packageVersionId% --target-org %testOrg% --wait 30 18 | 19 | REM Uninstall the dependencies 20 | sf package uninstall --package %packageVersionId% --target-org %testOrg% --wait 30 21 | -------------------------------------------------------------------------------- /scripts/05_test_exceptions.apex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/scripts/05_test_exceptions.apex -------------------------------------------------------------------------------- /scripts/06_test_successes.apex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/scripts/06_test_successes.apex -------------------------------------------------------------------------------- /scripts/08_setup_connection.bat: -------------------------------------------------------------------------------- 1 | REM 01 - Update the executing user 2 | sf org display user 3 | 4 | REM 02 - Update the executing user in the auth provider 5 | 6 | REM 03 - Update the force ignore file (Comment the connection section) 7 | 8 | REM 04 - Potentially remove or add the utl__ namespace in the permission set 9 | 10 | REM 05 - Deploy the custom implementation files 11 | sf project deploy start --source-dir force-demo-data 12 | 13 | REM 06 - Assign the permission set 14 | sf org assign permset --name "NHS_API_Named_Credentials" 15 | 16 | REM 07 - Fix the force ignore again 17 | 18 | REM 06 - Connect the external credential in setup 19 | REM 07 - Run test Apex -------------------------------------------------------------------------------- /scripts/10_debug.apex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfwberg/lightweight-oauth-jwt-client-credentials-auth-provider/4f8497b2b63aae02801cfce6340d9a371fc0970e/scripts/10_debug.apex -------------------------------------------------------------------------------- /scripts/dependencies.bat: -------------------------------------------------------------------------------- 1 | rem Install dependency on package "Lightweight - Auth Provider Util v2@0.3.0-1" - /packaging/installPackage.apexp?p0=04t4K000002Jv1tQAC 2 | rem this package is optional but required for error logging and user context support. 3 | sf package install -p "04t4K000002Jv1tQAC" -w 30 4 | -------------------------------------------------------------------------------- /scripts/package.bat: -------------------------------------------------------------------------------- 1 | rem CREATE A PACKAGE - UPDATE DEVHUB 2 | sf package create --name "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider" --description "A lightweight generic Auth Provider Apex class that can be used with named/external credentials to get an access token using the OAuth 2.0 JWT Client Credentials Authentication Flow." --package-type "Managed" --path "force-app" --target-dev-hub "[DEVHUB NAME]" 3 | 4 | rem CREATE A PACKAGE VERSION - UPDATE DEVHUB 5 | sf package version create --package "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider" --installation-key-bypass --code-coverage --target-dev-hub "[DEVHUB NAME]" -w 30 6 | 7 | rem PROMOTE THE PACKAGE VERSION - UPDATE NAME + DEVHUB 8 | sf package version promote --package "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.3.0-1" --target-dev-hub "[DEVHUB NAME]" 9 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app/package", 5 | "default": true, 6 | "package": "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider", 7 | "versionName": "Version 60.0 - 0.5", 8 | "versionNumber": "0.5.0.NEXT", 9 | "versionDescription": "A lightweight generic Auth Provider Apex class that can be used with named/external credentials to get an access token using the OAuth 2.0 JWT Client Credentials Authentication Flow.", 10 | "ancestorVersion": "0.4.0", 11 | "dependencies": [ 12 | { 13 | "package": "Lightweight - Apex Unit Test Util v2@2.4.0-1" 14 | }, 15 | { 16 | "package": "Lightweight - REST Util@0.11.0-1" 17 | } 18 | ] 19 | }, 20 | { 21 | "path": "force-app/package", 22 | "default": false, 23 | "package": "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider (Unlocked)", 24 | "versionName": "Version 60.0 - 0.5", 25 | "versionNumber": "0.5.0.NEXT", 26 | "versionDescription": "A lightweight generic Auth Provider Apex class that can be used with named/external credentials to get an access token using the OAuth 2.0 JWT Client Credentials Authentication Flow.", 27 | "dependencies": [ 28 | { 29 | "package": "Lightweight - Apex Unit Test Util v2 (Unlocked)@2.4.0-1" 30 | }, 31 | { 32 | "package": "Lightweight - REST Util (Unlocked)@0.11.0-1" 33 | } 34 | ] 35 | }, 36 | { 37 | "path": "force-demo-data", 38 | "default": false, 39 | "package": "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider - Demo Data", 40 | "versionName": "0.1", 41 | "versionNumber": "0.1.0.NEXT", 42 | "versionDescription": "This is a test data package to simply illustrate what the metadata would look like. API Keys and values are obfuscated here, but is should give a good idea. (PRIVATE USE ONLY)" 43 | } 44 | ], 45 | "name": "lightweight-oauth-jwt-client-credentials-auth-provider", 46 | "namespace": "lwt", 47 | "sfdcLoginUrl": "https://login.salesforce.com", 48 | "sourceApiVersion": "60.0", 49 | "packageAliases": { 50 | "Lightweight - Apex Unit Test Util v2@2.4.0-1": "04tP3000000M6OXIA0", 51 | "Lightweight - Apex Unit Test Util v2 (Unlocked)@2.4.0-1": "04tP3000000M6Q9IAK", 52 | "Lightweight - REST Util@0.11.0-1": "04tP3000000M6gHIAS", 53 | "Lightweight - REST Util (Unlocked)@0.11.0-1": "04tP3000000M6htIAC", 54 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider": "0Ho4K0000008OYeSAM", 55 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.1.0-1": "04t4K000002JuypQAC", 56 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.1.0-2": "04t4K000002JuyuQAC", 57 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.2.0-1": "04t4K000002Juz4QAC", 58 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.3.0-1": "04t4K000002Jv1yQAC", 59 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.4.0-1": "04tP3000000739FIAQ", 60 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider (Unlocked)": "0HoP300000000ODKAY", 61 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider@0.5.0-1": "04tP3000000MWfZIAW", 62 | "Lightweight - OAuth 2.0 JWT Client Credentials Auth Provider (Unlocked)@0.5.0-1": "04tP3000000MWndIAG" 63 | } 64 | } --------------------------------------------------------------------------------