I
recently had to implement a hybrid authorisation flow from a Xamarin client to IdentityServer 4. This involved making a browser request to IdentityServer’s
authorize endpoint to retrieve an authorisation code, and then making a REST request to IdentityServer’s
token endpoint, exchanging the authorisation code for an access token.
However, I found the documentation on the
token endpoint to be slightly lacking. It states that the token endpoint requires a POST request, along with a series of URL query parameters:
POST /connect/token
client_id=client1&
client_secret=secret&
grant_type=authorization_code&
code=hdh922&
redirect_uri=https://myapp.com/callback
Every attempt at making this request failed. On top of that I wasn’t keen on sending a client secret potentially in the clear. After some digging through the IdentityServer code I was able to construct a POST request that worked for me, which I verified using
Advanced REST Client. My POST request was:
POST /connect/token
grant_type=authorization_code&
code=hdh922&
redirect_uri=https://myapp.com/callback
The value of the
code parameter is the authorisation code retrieved from IdentityServer’s authorize endpoint, and the value of the
redirect_uri parameter is the callback URL registered with IdentityServer. Note that this isn’t the only way of making the POST request to the token endpoint, as IdentityServer permits several variations.
So what’s happened to the
client_id and
client_secret query parameters listed in the first POST request? While IdentityServer can accept them as query parameters, I was unhappy about sending my client secret as a query parameter. Luckily, IdentityServer also permits both
client_id and
client_secret to be sent in the
Authorization header of the POST request, encoded as a basic authentication value, which is what I did. For more information about basic authentication, see
Specifying Basic Authentication in a Web Request.
Translating this into code produces the
GetTokenAsync method, which makes the request using the
PostAsync method of the
RequestProvider class:
public async Task<UserToken> GetTokenAsync(string code)
{
string data = string.Format("grant_type=authorization_code&code={0}&redirect_uri={1}", code, WebUtility.UrlEncode(GlobalSetting.Instance.IdentityCallback));
var token = await _requestProvider.PostAsync<UserToken>(GlobalSetting.Instance.TokenEndpoint, data, GlobalSetting.Instance.ClientId, GlobalSetting.Instance.ClientSecret);
return token;
}
The
PostAsync method in the
RequestProvider class is shown in the following code example:
public async Task<TResult> PostAsync<TResult>(string uri, string data, string clientId, string clientSecret)
{
HttpClient httpClient = CreateHttpClient(string.Empty);
if (!string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret))
{
AddBasicAuthenticationHeader(httpClient, clientId, clientSecret);
}
var content = new StringContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
HttpResponseMessage response = await httpClient.PostAsync(uri, content);
await HandleResponse(response);
string serialized = await response.Content.ReadAsStringAsync();
TResult result = await Task.Run(() =>
JsonConvert.DeserializeObject<TResult>(serialized, _serializerSettings));
return result;
}
After creating the
HttpClient instance, the
PostAsync method calls the
AddBasicAuthenticationHeader method, which is shown in the following code example:
private void AddBasicAuthenticationHeader(HttpClient httpClient, string clientId, string clientSecret)
{
if (httpClient == null)
return;
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
return;
httpClient.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(clientId, clientSecret);
}
The
AddBasicAuthenticationHeader method adds the
Authorization header to the POST request, with its value being the
clientId and
clientSecret values encoded using basic authentication.
Back in the
PostAsync method, the other piece of the puzzle that was missing from the IdentityServer documentation is that the
ContentType of the POST request must be set to
application/x-www-form-urlencoded. I suspect this is the missing piece of the puzzle that caused the original request to fail.
To see the full application from which I’ve taken this code, along with more content about using IdentityServer from a Xamarin client, see
Enterprise Application Patterns using Xamarin.Forms.
No comments:
Post a Comment