Having Azure API Gateway with an exteral IdP (Okta) we setup a simple and working setup.
The API Gateway is able to authenticate and authorize the JWT token and call the backend service (App Logic or Azure function).
Next to the invoking the backend services, the API Gateway can pass claims from the JWT token
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="No JWT token" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true" clock-skew="10" output-token-variable-name="jwttoken">
<set-header name="jwt-token" exists-action="append">
<value><![CDATA[#(context.Variables.ContainsKey("jwttoken") ? (((Jwt)(context.Variables["jwttoken"])).Subject) : "") ]]></value>
</set-header>
The backend services (AppLogic) are authorized using its SAS token, so we have to remove the Authorization header. I'm considering to send the JWT token to the backend anyway, but so far I found no way to do so (serialize the jwttoken variable, the API GW doesn't allow to call the .ToString() method).
Q: Is there / How do I send the original JWT token to the backend (in another header?)
Jwt.TryParse can be used to break up the JWT token and pass on in another header in clear text:
<policies>
<inbound>
<base />
<set-header name="parsed-token" exists-action="override">
<value>#{
string parsedToken = "error";
string tokenHeader = context.Request.Headers.GetValueOrDefault("jwt-token", "");
if (tokenHeader?.Length > 0)
{
Jwt jwt;
if (tokenHeader.TryParseJwt(out jwt))
{
foreach(var claim in jwt.Claims)
{
parsedToken += claim.Key + ":" + string.Join("-",claim.Value) + ";";
}
}
}
return parsedToken;
}</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
For a sample token from https://jwt.io/#debugger-io this would give you headers like:
"jwt-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"parsed-token": "errorsub:1234567890;name:John Doe;iat:1516239022;",
I still cannot find proper documentation for class Jwt used in Azure API policy expressions - this class does not seem to have a ToString().
Since your backend expects SAS token at some point in your policy you have set-header policy to set SAS token, correct? Well, then just copy value from Authorization header before you overwrite it:
<set-header name="jwt" exists-action="override">
<value>#(context.Request.Headers.GetValueOrDefault("Authorization"))</value>
</set-header>
Related
hope you are doing well !
I have been trying to write a GraphQL Mutation Resolver for a REST POST request in Azure APIM but nothing is working so far.
The REST call takes an object with firstName, lastName, username, password and returns an object with the same fields.
Below is my current code. Any Help would be appreciated.
<set-graphql-resolver parent-type="Mutation" field="createUser">
<http-data-source>
<http-request>
<set-method>POST</set-method>
<set-url>[URL]</set-url>
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-body>#{
var args = context.Request.Body.As<JObject>(true)["arguments"];
JObject jsonObject = new JObject();
jsonObject.Add("firstName", args["firstName"]);
jsonObject.Add("lastName", args["lastName"]);
jsonObject.Add("username", args["username"]);
jsonObject.Add("password", args["password"]);
return jsonObject.ToString();
}</set-body>
</http-request>
</http-data-source>
</set-graphql-resolver>
UPDATE:
This is the schema i am using:
And this is how i am testing the mutation with the arguments and the original error i am getting:
In Application insights, i am getting this error log:
NOTE: This is the original response that i'm getting from a normal REST Request
Thank you!
I contacted Microsoft about this issue and they told me that it is a bug on the service side.
The product team is working on the fix and it will be released in v.33 (4 to 6 weeks).
Currently we have the front end sending files to an Azure storage account into specific blob containers. The front-end is manually getting SAS tokens put into the build via a person getting a SAS from the storage account and pasting it into the front-end code so it can read and write to the storage account.
We're wanting to have the front-end send a request to APIM with a file. We then want to hash that file, use that hash as the name and store it in azure blob storage. I'm new to Azure API Management, is this even possible? It seems like I can't get at the uploaded file.
In APIM policies I currently have the Authorization to the storage account working but I can't figure out how to get at the Request.Files like I normally would in an MVC app.
I've been looking all over https://learn.microsoft.com/ as well as https://techcommunity.microsoft.com/ and SO and I've even started looking on the second page of Google search results. I can't find anything that points to this being possible or not.
Here is my current policy. It works in the sense that the front-end can hit it and pass through a file and that file is saved. But we want to hash the file and use that hash as the name to avoid name collisions in the Azure storage account blob container
<policies>
<inbound>
<base />
<set-variable name="UTCNow" value="#(DateTime.UtcNow.ToString("R"))" />
<set-variable name="Verb" value="#(context.Request.Method)" />
<set-variable name="documentstorage" value="{{documentstorage}}" />
<set-variable name="documentstoragekey" value="{{documentstorageaccesskey}}" />
<set-variable name="version" value="2019-12-12" />
<set-variable name="bodySize" value="#(context.Request.Headers["Content-Length"][0])" />
<set-variable name="contentType" value="#(context.Request.Headers["Content-Type"][0])" />
<set-header name="x-ms-version" exists-action="override">
<value>#((string)context.Variables["version"] )</value>
</set-header>
<set-header name="x-ms-blob-type" exists-action="override">
<value>BlockBlob</value>
</set-header>
<set-header name="date" exists-action="override">
<value>#((string)context.Variables["UTCNow"])</value>
</set-header>
<set-header name="Authorization" exists-action="override">
<value>#{
var account = (string)context.Variables["documentstorage"];
var key = (string)context.Variables["documentstoragekey"];
var verb = (string)context.Variables["Verb"];
var container = context.Request.MatchedParameters["container"];
var fileName = context.Request.MatchedParameters["fileName"];
var dateNow = (string)context.Variables["UTCNow"];
string contentType = (string)context.Variables["contentType"];//"application/pdf";
var contentLength = (string)context.Variables["bodySize"];
var stringToSign = string.Format("{0}\n\n\n{1}\n\n{2}\n{3}\n\n\n\n\n\nx-ms-blob-type:BlockBlob\nx-ms-version:{4}\n/{5}/{6}/{7}",
verb,
contentLength,
contentType,
(string)context.Variables["UTCNow"],
(string)context.Variables["version"],
account,
container,
fileName);
string signature = "";
var unicodeKey = Convert.FromBase64String(key);
using (var hmacSha256 = new HMACSHA256(unicodeKey))
{
var dataToHmac = Encoding.UTF8.GetBytes(stringToSign);
signature = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac));
}
var authorizationHeader = string.Format(
"{0} {1}:{2}",
"SharedKey",
account,
signature);
return authorizationHeader;
}</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
I haven't tried this but it sounds like you can get the request body (that's where I assume your file is):
var inBody = context.Request.Body.As<byte[]>(preserveContent: true);
Based on this: https://learn.microsoft.com/en-us/azure/api-management/api-management-policy-expressions#ref-imessagebody and this https://learn.microsoft.com/en-us/azure/api-management/api-management-transformation-policies#SetBody
However, if you just want to get a unique files names, why not simply generate a GUID? Or do you mean you want to make sure that every file only gets uploaded once? (Then hashing probably makes sense)
I've followed this post in relation to this example and made the additions to the portal and code as suggested:
Portal: Web API - Expose an API - Add Scope for "offline_access" (also tried with format "demo.offline_access"); Web App added "offline_access" permission
Code: Added offline_access to Start_Auth.cs, Global.cs and both Web.config files.
I make my request for accessToken and refreshToken as per documentation with:
var ClientID = "XXXX";
var BaseURL = "https://XXX.b2clogin.com/XXX.onmicrosoft.com/oauth2/v2.0/token?p=B2C_1_signinsignoutpolicy";
var ClientSecret = "XXX";
string redirectUri = "https://XXX.azurewebsites.net/api/GetAccessToken";
var content = new StringContent(
"&grant_type=authorization_code"+
"&client_id="+ClientID+
"&scope=" + "https://XXX.onmicrosoft.com/api/demo.read https://XXX.onmicrosoft.com/api/demo.write https://XXX.onmicrosoft.com/api/offline_access" +
"&code="+authCode+
"&redirect_uri=" + redirectUri+
"&client_secret=" + ClientSecret,
Encoding.UTF8,
"application/x-www-form-urlencoded");
var response = await httpClient.PostAsync(BaseURL, content);
var output = await response.Content.ReadAsStringAsync();
output returns access_token but no refresh token. When looking at the claims of the access_token, scope (scp) correctly shows offline_access demo.read demo.write
What am I missing to get refresh token?
[EDIT}
Here's postman result:
JWT for access token received:
WebApp API Permissions:
WebAPI - Expose an API:
Code changes:
Both WebApp and WebAPI Web.config scopes:
<add key="api:ReadScope" value="demo.read" />
<add key="api:WriteScope" value="demo.write" />
<add key="api:OfflineScope" value="offline_access" />
TaskWebApp Globals.cs scope addition:
// API Scopes
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
public static string OfflineTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:OfflineScope"];
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope, OfflineTasksScope };
TaskWebApp Startup.Auth.cs scope addition at ConfigureAuth:
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope} {Globals.OfflineTasksScope}"
Thank you for your help
https://XXX.onmicrosoft.com/api/offline_access is a permission you customized in your web api app. It's not for getting a refresh token.
You just need to use offline_access here.
"&scope=" + "https://XXX.onmicrosoft.com/api/demo.read https://XXX.onmicrosoft.com/api/demo.write offline_access"
I'm trying to log the requests and responses from API Management Gateway to Azure Event Hubs.I'm using "log-to-event-hub" policy for that.I want to send a single event to event hub containing both the request and response together.
I tried including the event-hub policy inside the inbound policy with both the request and response together but I'm only getting the request and not the response.Similarly I tried including it in the outbound policy but got only the response.As I'm sending the event-hub logs to Azure Log Analytics I wanted to get the complete request and response together.I know that keeping the "log-to-event-hub" policy in both inbound and outbound policies will give me two different log events.
<inbound>
<set-variable name="message-id" value="#(Guid.NewGuid())" />
<log-to-eventhub logger-id="all-logs" partition-id="0">#{
var requestLine = string.Format("{0} {1} HTTP/1.1\r\n",
context.Request.Method,
context.Request.Url.Path + context.Request.Url.QueryString);
var body = "Request " + context.Request.Body?.As<string>(true) + "Response " + context.Response.Body?.As<string>(true);
var headers = context.Request.Headers
.Where(h => h.Key != "Authorization" && h.Key != "Ocp-Apim-Subscription-Key")
.Select(h => string.Format("{0}: {1}", h.Key, String.Join(", ", h.Value)))
.ToArray<string>();
var headerString = (headers.Any()) ? string.Join("\r\n", headers) + "\r\n" : string.Empty;
return "staging: " + context.Response.StatusCode + " " + context.Variables["message-id"] + "\n"
+ requestLine + headerString + "\r\n" + body;
}</log-to-eventhub>
</inbound>
Is it possible to have both in the same event and only one event is being logged.
I would capture the required values from request in a variable, combine it with request values in the outbound policy and log it there:
<policies>
<inbound>
<base />
<set-variable name="requestHeaders" value="#(JsonConvert.SerializeObject(context.Request.Headers))" />
<set-variable name="requestBody" value="#(context.Request.Body.As<string>(true))" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<log-to-eventhub logger-id="testlogger">#{
var content = new JObject();
content["reqHeaders"] = context.Variables.GetValueOrDefault<string>("requestHeaders");
content["reqBody"] = context.Variables.GetValueOrDefault<string>("requestBody");
content["resStatus"] = JsonConvert.SerializeObject(context.Response.StatusCode);
content["resBody"] = context.Response.Body.As<string>(true);
return content.ToString();
}</log-to-eventhub>
</outbound>
<on-error>
<base />
</on-error>
</policies>
I have created a web api that uses the JWT system using this article here. When calling the API from a REST client it works just fine. However when trying to access it from a browser it gives a CORS error since it doesn't send out the correct response headers.
Startup.cs
app.UseCors(CorsOptions.AllowAll);
Note that on my controllers CORS works just fine, it just breaks for the OAuthAuthorizationServer.
CustomOAuthProvider.cs
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
var user = Database.Users.FirstOrDefault(u => u.Email == context.UserName);
if (user == null || !BCrypt.Net.BCrypt.Verify(context.Password, user.Password))
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return Task.FromResult<object>(null);
}
var companyId = int.Parse(context.OwinContext.Get<string>("company_id"));
var company = user.Companies.FirstOrDefault(c => c.Id == companyId);
if (company == null)
{
context.SetError("invalid_grant", "You don't belong to that company!");
return Task.FromResult<object>(null);
}
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim("uue", user.Email));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "audience", company.ServerUrl }
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
return Task.FromResult<object>(null);
}
However after making the call to obtain the token, I only get back these response headers.
Content-Length:1245
Content-Type:text/html
Date:Wed, 20 Apr 2016 20:34:40 GMT
Server:Microsoft-IIS/8.5
X-Powered-By:ASP.NET
Is there something I'm doing wrong?
Note: I'm assuming you are using the same Startup.cs code defined in the liked tutorial.
Try to move the call to app.UseCors(CorsOptions.AllowAll); at the top of your Configuration method in Startup.cs:
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
HttpConfiguration config = new HttpConfiguration();
// Web API routes
config.MapHttpAttributeRoutes();
ConfigureOAuth(app);
app.UseWebApi(config);
}
In Owin every middleware in the pipeline is executed only if the preceding passes through the invocation. For this reason app.UseCors is executed only after the AuthenticationMiddleware (in your case OAuthAuthorizationServer) and only if it does not stop the flow in the pipeline (e.g. OAuth returns a response).
Moving the Cors middleware declaration before other middlewares ensures you that it is executed for each request.
Make sure you allow CORS in web config
<httpProtocol>
<customHeaders>
<clear />
<add name="Access-Control-Allow-Methods" value="GET,PUT,POST,OPTIONS,DEBUG" />
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="authorization,content-type" />
</customHeaders>
</httpProtocol>