in src/Microsoft.Health.Fhir.Api/Controllers/AadSmartOnFhirProxyController.cs [287:447]
public async Task<ActionResult> Token(
[FromForm(Name = "grant_type")] string grantType,
[FromForm(Name = "code")] string compoundCode,
[FromForm(Name = "redirect_uri")] Uri redirectUri,
[FromForm(Name = "client_id")] string clientId,
[FromForm(Name = "client_secret")] string clientSecret)
{
try
{
EnsureArg.IsNotNull(grantType, nameof(grantType));
}
catch (ArgumentNullException ex)
{
throw new AadSmartOnFhirProxyBadRequestException(string.Format(Resources.ValueCannotBeNull, ex.ParamName), ex);
}
#pragma warning disable CA2000 //https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#httpclient-and-lifetime-management
var client = _httpClientFactory.CreateClient();
#pragma warning restore CA2000
if (Request != null &&
string.IsNullOrEmpty(clientId) &&
Request.Headers.TryGetValue("Authorization", out var authHeaderValues))
{
var authHeader = authHeaderValues.ToString();
if (authHeader.Contains("Basic ", StringComparison.OrdinalIgnoreCase))
{
var basicTokenEncoded = authHeader.Remove(0, "Basic ".Length);
var basicToken = Base64UrlEncoder.Decode(basicTokenEncoded);
var basicTokenSplit = basicToken.Split(":");
if (basicTokenSplit.Length == 2)
{
clientId = basicTokenSplit[0];
clientSecret = basicTokenSplit[1];
}
}
}
// Azure AD supports client_credentials, etc.
// These are used in tests and may have value even when SMART proxy is used.
// This prevents disabling those options.
// TODO: This should probably removed.
// If somebody is accessing the IDP through this endpoint, all things not SMART on FHIR should be errors.
// For now, we will keep it to keep the E2E tests from failing.
// TODO: Add handling of 'aud' -> 'resource', should that be an error or should translation be done?
if (grantType != "authorization_code")
{
var fields = new List<KeyValuePair<string, string>>();
foreach (var f in Request.Form)
{
fields.Add(new KeyValuePair<string, string>(f.Key, f.Value));
}
using var passThroughContent = new FormUrlEncodedContent(fields);
var passThroughResponse = await client.PostAsync(new Uri(_aadTokenEndpoint), passThroughContent);
return new ContentResult()
{
Content = await passThroughResponse.Content.ReadAsStringAsync(),
StatusCode = (int)passThroughResponse.StatusCode,
ContentType = "application/json",
};
}
try
{
EnsureArg.IsNotNull(compoundCode, nameof(compoundCode));
EnsureArg.IsNotNull(redirectUri, nameof(redirectUri));
EnsureArg.IsNotNull(clientId, nameof(clientId));
}
catch (ArgumentNullException ex)
{
throw new AadSmartOnFhirProxyBadRequestException(string.Format(Resources.ValueCannotBeNull, ex.ParamName), ex);
}
JObject decodedCompoundCode;
string code;
try
{
decodedCompoundCode = JObject.Parse(Base64UrlEncoder.Decode(compoundCode));
code = decodedCompoundCode["code"].ToString();
}
catch (Exception ex)
{
_logger.LogError("Error decoding compound code: {message}", ex.Message);
throw new AadSmartOnFhirProxyBadRequestException(Resources.InvalidCompoundCode, ex);
}
Uri callbackUrl = _urlResolver.ResolveRouteNameUrl(RouteNames.AadSmartOnFhirProxyCallback, new RouteValueDictionary { { "encodedRedirect", Base64UrlEncoder.Encode(redirectUri.ToString()) } });
var formValues = new List<KeyValuePair<string, string>>(
new[]
{
new KeyValuePair<string, string>("grant_type", grantType),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("redirect_uri", callbackUrl.AbsoluteUri),
new KeyValuePair<string, string>("client_id", clientId),
});
if (!string.IsNullOrEmpty(clientSecret))
{
formValues.Add(new KeyValuePair<string, string>("client_secret", clientSecret));
}
using var content = new FormUrlEncodedContent(formValues);
HttpResponseMessage response = await client.PostAsync(new Uri(_aadTokenEndpoint), content);
if (!response.IsSuccessStatusCode)
{
return new ContentResult()
{
Content = await response.Content.ReadAsStringAsync(),
StatusCode = (int)response.StatusCode,
ContentType = "application/json",
};
}
var tokenResponse = JObject.Parse(await response.Content.ReadAsStringAsync());
// Handle fields passed through launch context
foreach (var launchField in _launchContextFields)
{
if (decodedCompoundCode[launchField] != null)
{
tokenResponse[launchField] = decodedCompoundCode[launchField];
}
}
tokenResponse["client_id"] = clientId;
// Replace fully qualifies scopes with short scopes and replace $
string[] scopes = tokenResponse["scope"]?.ToString().Split(' ');
if (scopes != null)
{
var scopesBuilder = new StringBuilder();
foreach (var s in scopes)
{
if (IsAbsoluteUrl(s))
{
var scopeUri = new Uri(s);
scopesBuilder.Append($"{scopeUri.Segments.Last().Replace('$', '/')} ");
}
else
{
scopesBuilder.Append($"{s.Replace('$', '/')} ");
}
}
tokenResponse["scope"] = scopesBuilder.ToString().TrimEnd(' ');
}
return new ContentResult()
{
Content = tokenResponse.ToString(Newtonsoft.Json.Formatting.None),
StatusCode = (int)response.StatusCode,
ContentType = "application/json",
};
}