Basic example- renew an access token via a refresh token
Objective
You have an external application that requires user login. You want to acquire an access token on behalf of the logged user.
Because the access token has a validity of 1 hour, you want to renew it, instead of forcing the user to log in again.
Or,
- Your external app is an interactive application (at least for the user to log in).
- It will be authenticated and authorized via the ERP.net login form (on behalf of an ERP.net internal user).
- Your external application will access the ERP.net instance on behalf of the logged user.
- Will work with ERP.net internal users and it's able to keep a secret, so it's also a condfidential application.
- There'll be user interaction (because of the login), so your external app will use authorization code flow.
- Aditionally, you want to renew the access token when it expires, instead of forcing the user to login in.
Remarks
This example uses as a basis the following one: Basic example- exchange an auth code for an access token.
The reason for this is simple. The referened example already shows how an access token is obtained, along with a refresh token. In fact, it's more accurate to say- your first access token with its corresponding refresh token. Your "first" access token, because it will change when you renew it (i.e., you'll obtain a new one).
Note
Refreshing the access token only makes sense in the authorization code flow. This will help the user by not having to enter their credentials each time their access token expires.
In flows such as client_credentials refreshing an access token is pointless, because you can always obtain a new one with a single request, directly to the token endpoint.
Prerequisites
You have a trusted application, defined as follows:
| Attribute | Value | Comment |
|---|---|---|
| Name | My first trusted app | This value doesn't matter much. It's used for user-friendly identification. |
| ApplicationUri | my.trusted.app/first | This is your trusted app's unique identifier. It's used in the authentication process. |
| IsEnabled | true | |
| ImpersonateAsInternalUserAllowed | true | The trusted application will allow authentication from internal users. |
| ImpersonateLoginUrl | http://localhost/signin-callback | The url where your external app is listening. Redirection to this uri will be performed after the user logs in successfully. |
| ClientType | Confidential | Your external app "will work" with internal users only, so there'll be no "public" acccess. We can assume that it can keep a secret securely (in fact, it's a must). |
| ApplicationSecretHash | <base64(sha256(your-secret))> |
The external app's secret. |
All other attributes can have their default values. They are not covered by this example.
Steps
Authorize endpoint
You just need to call a simple GET request.
GET /id/connect/authorize?
client_id=my.trusted.app/first&
redirect_uri=http://localhost/signin-callback&
response_type=code id_token&
scope=openid profile offline_access DomainApi&
nonce=abc&
state=xyz HTTP/1.1
Host: demodb.my.erp.net
If everything is OK, the following will happen:
- A redirect to the ERP.net login page will be made.
- After the user logs in successfully, a redirect back to your external app will be performed.
Sign in callback
The previous step leads here. You'll receive a GET request such as:
GET /signin-callback?
code=g0ZGZmNjVmOWI&
state=dkZmYxMzE2 HTTP/1.1
Host: localhost
Where the code in the uri query is your authorization code.
Now you're ready to exchange this authorization code for an access token.
Token endpoint
Once you have the authorization code, obtaining the access token is pretty easy. Just make the following POST request.
POST /id/connect/token HTTP/1.1
Host: demodb.my.erp.net
Content-Type: application/x-www-form-urlencoded
client_id=my.trusted.app/first&
client_secret=<my_plain_app_secret>&
grant_type=authorization_code&
code=g0ZGZmNjVmOWI&
redirect_uri=http://localhost/signin-callback
That's all. You'll receive something like this:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJEbGhqYjhzOEUySm1tcWg2UDlxZEEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE2NTUwNTc3MjQsImV4cCI6MTY1NTA1ODAyNCwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJwayIsIm5vbmNlIjoiYWJjIiwiaWF0IjoxNjU1MDU3NzI0LCJhdF9oYXNoIjoibUhfSUZEUVppRHdZb2h5a0FZR2NJZyIsInNfaGFzaCI6IlRjMWtiNVB1U2lheEN2NXVJZHZ6ZlEiLCJzaWQiOiJ1Q3FiZkI4OHpYMXUzOW40NERwVjFRIiwic3ViIjoiYWRtaW4iLCJhdXRoX3RpbWUiOjE2NTUwMjc3MTksImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.CzTk7SXiqcgpjVXCvdgDKJ92bt2a93R76l5WmCIZ6hMG6VDYHXlkBlqmG15l8Zsc1SpLn949f-OQn4nK1LaLkOA1rrMfT6lhMdrdBkQED7mYrjTyRqUJHnkriYpLsbL4Ze5gOP1M0HlDi6ZWjhZyzJgEyqi_T44lmlyZc0ujQ0Zba-_afXV7VpmgL9dIPwSmhuP14x2UJIGziBE8m23DL4GqTMQYgX0HNGLa2Tgiztp4h9ABBWWhj_iEKJ3ZoZ3CfMVMn53fqDaf9fuIrgYrOOTKqE7UrxH2bhdLUlaqka7KeGIsRd7f6wV2XqFDfY3vtW85CzQnjuGhj-qAJoZjCw",
"access_token": "eycccGciOiJSUzI1NiIsImtpZCI6IkJEbGhqYjhzOEUySm1tcWg2UDlxZEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE2NTUwNTc3MjQsImV4cCI6MTY1NTA2MTMyNCwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJEb21haW5BcGkiLCJjbGllbnRfaWQiOiJwayIsInN1YiI6ImFkbWluIiwiYXV0aF90aW1lIjoxNjU1MDI3NzE5LCJpZHAiOiJsb2NhbCIsImlkIjoiOWRhNjQ4MzktYThkMC00OTFkLWFlYmItNGQxOGZhNDJiMDE0IiwibmFtZSI6IlNQUyIsImVtYWlsIjoiYWRtaW5AbWFpbC5jb20iLCJ1c2VyX3R5cGUiOiJJbnRlcm5hbFVzZXIiLCJpc19hZG1pbi1ee6InRydWUiLCJlbWFpbF92ZXJpZmllZCI6ImZhb2NlIiwiZGIiOiJFMV9ERVYiLCJsb2NhbGUiOiJieyIsImp0aSI6Il9sdEJMS3djSlNLbUFhM25mbFpwNFEiLCJzaWQiOiJ1Q3FiZds2898pYMXUzOW40NqRwVjFRIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIww1kRvbWFpbkFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.AzHxj_iBM3bfcOtdaSNHNbPUHGCf0JAo7fV1fo9JT-rqCHjc0t8VEa1qO5R2jemvs7vDBf6GARxgul3pAy7YQpmqzzruswoLDkDdUMX1LXzHLgp0ppYoNa1A_M_O4UTXCe7xGBRHSyRRQLGsTTGMkv1pK0E3Xn3rAfOPvo4wfrQ8QabVcdA7mupY4qF01tIHPv_7NGS2SyPfCVdAYcxUy8HpQ-RdoXMaVWVz_JhXgMNZ9_nFTxedPGakZJMDjnvYss_GKjucbeYdZM9jSrqEmXDw6s8A3o1jKOyurzIBzug55Dxee8UBWepcO5S08GPguBFotamUvStMdDY0KkmZYvw",
"expires_in": 3600,
"refresh_token": "6-Cv7vQ5ouhYzs0AWg6tsG-YK7O5xP_kb5Qb8wEJMnw",
"scope": "openid profile DomainApi offline_access"
}
As you see, the result contains the following:
- Your first access token.
- When it will expire (in seconds).
- The corresponding refresh token.
Authorized Domain API call
Now you're authorized and we can make a legitimate call to the ERP.net Domain Api. E.g.,
GET /api/domain/odata/Crm_Customers?$top=10 HTTP/1.1
Host: demodb.my.erp.net
Authorization: Bearer eycccGciOiJSUzI1NiIsImtpZCI6IkJEbGhqYjhzOEUySm1tcWg2UDlxZEEiLCJ0eXAiOiJhdCtqd3QifQ.eyJuYmYiOjE2NTUwNTc3MjQsImV4cCI6MTY1NTA2MTMyNCwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJEb21haW5BcGkiLCJjbGllbnRfaWQiOiJwayIsInN1YiI6ImFkbWluIiwiYXV0aF90aW1lIjoxNjU1MDI3NzE5LCJpZHAiOiJsb2NhbCIsImlkIjoiOWRhNjQ4MzktYThkMC00OTFkLWFlYmItNGQxOGZhNDJiMDE0IiwibmFtZSI6IlNQUyIsImVtYWlsIjoiYWRtaW5AbWFpbC5jb20iLCJ1c2VyX3R5cGUiOiJJbnRlcm5hbFVzZXIiLCJpc19hZG1pbi1ee6InRydWUiLCJlbWFpbF92ZXJpZmllZCI6ImZhb2NlIiwiZGIiOiJFMV9ERVYiLCJsb2NhbGUiOiJieyIsImp0aSI6Il9sdEJMS3djSlNLbUFhM25mbFpwNFEiLCJzaWQiOiJ1Q3FiZds2898pYMXUzOW40NqRwVjFRIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIww1kRvbWFpbkFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.AzHxj_iBM3bfcOtdaSNHNbPUHGCf0JAo7fV1fo9JT-rqCHjc0t8VEa1qO5R2jemvs7vDBf6GARxgul3pAy7YQpmqzzruswoLDkDdUMX1LXzHLgp0ppYoNa1A_M_O4UTXCe7xGBRHSyRRQLGsTTGMkv1pK0E3Xn3rAfOPvo4wfrQ8QabVcdA7mupY4qF01tIHPv_7NGS2SyPfCVdAYcxUy8HpQ-RdoXMaVWVz_JhXgMNZ9_nFTxedPGakZJMDjnvYss_GKjucbeYdZM9jSrqEmXDw6s8A3o1jKOyurzIBzug55Dxee8UBWepcO5S08GPguBFotamUvStMdDY0KkmZYvw
Access token renewal
When your access token expires, all requests will begin to return HTTP 401 - Unauthorized. So, now's the time to renew your access token. You can do it via the token endpoint, this way:
POST /id/connect/token HTTP/1.1
Host: demodb.my.erp.net
Content-Type: application/x-www-form-urlencoded
client_id=my.trusted.app/first&
client_secret=<my_plain_app_secret>&
grant_type=refresh_token&
refresh_token=6-Cv7vQ5ouhYzs0AWg6tsG-YK7O5xP_kb5Qb8wEJMnw
Done. If everything is correct, you'll get a response like this:
{
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJEbGhqYjhzOEUySm1tcWg2UDlxZEEiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE2NTUwNTc3MjQsImV4cCI6MTY1NTA1ODAyNCwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOiJwayIsIm5vbmNlIjoiYWJjIiwiaWF0IjoxNjU1MDU3NzI0LCJhdF9oYXNoIjoibUhfSUZEUVppRHdZb2h5a0FZR2NJZyIsInNfaGFzaCI6IlRjMWtiNVB1U2lheEN2NXVJZHZ6ZlEiLCJzaWQiOiJ1Q3FiZkI4OHpYMXUzOW40NERwVjFRIiwic3ViIjoiYWRtaW4iLCJhdXRoX3RpbWUiOjE2NTUwMjc3MTksImlkcCI6ImxvY2FsIiwiYW1yIjpbInB3ZCJdfQ.CzTk7SXiqcgpjVXCvdgDKJ92bt2a93R76l5WmCIZ6hMG6VDYHXlkBlqmG15l8Zsc1SpLn949f-OQn4nK1LaLkOA1rrMfT6lhMdrdBkQED7mYrjTyRqUJHnkriYpLsbL4Ze5gOP1M0HlDi6ZWjhZyzJgEyqi_T44lmlyZc0ujQ0Zba-_afXV7VpmgL9dIPwSmhuP14x2UJIGziBE8m23DL4GqTMQYgX0HNGLa2Tgiztp4h9ABBWWhj_iEKJ3ZoZ3CfMVMn53fqDaf9fuIrgYrOOTKqE7UrxH2bhdLUlaqka7KeGIsRd7f6wV2XqFDfY3vtW85CzQnjuGhj-qAJoZjCw",
"access_token": "eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE2NTU2NTM3MTYsImV4cCI6MTY1NTY1NzMxNiwiaXNzIjoiaHR0cHM6Ly9lMS1kZXYubG9jYWwvaWQiLCJhdWQiOlsiRG9tYWluQXBpIiwidXBkYXRlIl0sImNsaWVudF9pZCI6InBrIiwiY2xpZW50X2RiIjoiRTFfREVWIiwianRpIjoiT0pycTFjX1owNzBUbHZJVTVoUlhFZyIsInNjb3BlIjpbIkRvbWFpbkFwaSIsInVwZGF0ZSJdfQ.syDIwziTNy1m2XNSSKD_E8wScuuuS2ZENzaxdd9ClOU",
"expires_in": 3600,
"refresh_token": "SvQvQ9cxcYzs0AWg6tsGW1-YK7O5xP_k98868wEEMjr",
"scope": "openid profile DomainApi offline_access"
}
A new pair of access and refresh tokens. You should use them now.
If you've noticed, refreshing the access token is the same as getting it the first time. The only difference is,
grant_type=authorization_code&code=xxxto initially obtain the access token.grant_type=refresh_token&refresh_token=xxxwhen you want to acquire a new one - i.e., to refresh it.
Resources
Basic example- exchange an auth code for an access token
Authorization code flow with a web based external application
Authorization code flow with a console based external application
--
https://docs.erp.net/dev/topics/authentication/authentication-flows.html
https://docs.erp.net/dev/topics/authentication/trusted-applications.html
https://docs.erp.net/dev/domain-api/authentication.html
https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow