I'm building a system consisting of an Angular2 single page app and a REST API running on ECS. The API runs on .Net/Nancy, but that might well change.
I would like to give Cognito a try and this is how I imagined the authentication workflow:
SPA signs in user and receives a JWT
SPA sends JWT to REST API with every request
REST API verfies that the JWT is authentic
My question is about step 3. How can my server (or rather: my stateless, auto-scaled, load-balanced Docker containers) verify that the token is authentic? Since the "server" hasn't issued the JWT itself, it can't use its own secret (as described in the basic JWT example here).
I have read through the Cognito docs and googled a lot, but I can't find any good guideline about what to do with the JWT on the server side.
Turns out I didn't read the docs right. It's explained here (scroll down to "Using ID Tokens and Access Tokens in your Web APIs").
The API service can download Cognito's secrets and use them to verify received JWT's. Perfect.
Edit
#Groady's comment is on point: but how do you validate the tokens? I'd say use a battle-tested library like jose4j or nimbus (both Java) for that and don't implement the verification from scratch yourself.
Here's an example implementation for Spring Boot using nimbus that got me started when I recently had to implement this in java/dropwizard service.
Here's a way to verify the signature on NodeJS:
var jwt = require('jsonwebtoken');
var jwkToPem = require('jwk-to-pem');
var pem = jwkToPem(jwk);
jwt.verify(token, pem, function(err, decoded) {
console.log(decoded)
});
// Note : You can get jwk from https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Execute an Authorization Code Grant Flow
Assuming that you:
have correctly configured a user pool in AWS Cognito, and
are able to signup/login and get an access code via:
https://<your-domain>.auth.us-west-2.amazoncognito.com/login?response_type=code&client_id=<your-client-id>&redirect_uri=<your-redirect-uri>
Your browser should redirect to <your-redirect-uri>?code=4dd94e4f-3323-471e-af0f-dc52a8fe98a0
Now you need to pass that code to your back-end and have it request a token for you.
POST https://<your-domain>.auth.us-west-2.amazoncognito.com/oauth2/token
set your Authorization header to Basic and use username=<app client id> and password=<app client secret> per your app client configured in AWS Cognito
set the following in your request body:
grant_type=authorization_code
code=<your-code>
client_id=<your-client-id>
redirect_uri=<your-redirect-uri>
If successful, your back-end should receive a set of base64 encoded tokens.
{
id_token: '...',
access_token: '...',
refresh_token: '...',
expires_in: 3600,
token_type: 'Bearer'
}
Now, according to the documentation, your back-end should validate the JWT signature by:
Decoding the ID token
Comparing the local key ID (kid) to the public kid
Using the public key to verify the signature using your JWT library.
Since AWS Cognito generates two pairs of RSA cryptograpic keys for each user pool, you need to figure out which key was used to encrypt the token.
Here's a NodeJS snippet that demonstrates verifying a JWT.
import jsonwebtoken from 'jsonwebtoken'
import jwkToPem from 'jwk-to-pem'
const jsonWebKeys = [ // from https://cognito-idp.us-west-2.amazonaws.com/<UserPoolId>/.well-known/jwks.json
{
"alg": "RS256",
"e": "AQAB",
"kid": "ABCDEFGHIJKLMNOPabc/1A2B3CZ5x6y7MA56Cy+6ubf=",
"kty": "RSA",
"n": "...",
"use": "sig"
},
{
"alg": "RS256",
"e": "AQAB",
"kid": "XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=",
"kty": "RSA",
"n": "...",
"use": "sig"
}
]
function validateToken(token) {
const header = decodeTokenHeader(token); // {"kid":"XYZAAAAAAAAAAAAAAA/1A2B3CZ5x6y7MA56Cy+6abc=", "alg": "RS256"}
const jsonWebKey = getJsonWebKeyWithKID(header.kid);
verifyJsonWebTokenSignature(token, jsonWebKey, (err, decodedToken) => {
if (err) {
console.error(err);
} else {
console.log(decodedToken);
}
})
}
function decodeTokenHeader(token) {
const [headerEncoded] = token.split('.');
const buff = new Buffer(headerEncoded, 'base64');
const text = buff.toString('ascii');
return JSON.parse(text);
}
function getJsonWebKeyWithKID(kid) {
for (let jwk of jsonWebKeys) {
if (jwk.kid === kid) {
return jwk;
}
}
return null
}
function verifyJsonWebTokenSignature(token, jsonWebKey, clbk) {
const pem = jwkToPem(jsonWebKey);
jsonwebtoken.verify(token, pem, {algorithms: ['RS256']}, (err, decodedToken) => clbk(err, decodedToken))
}
validateToken('xxxxxxxxx.XXXXXXXX.xxxxxxxx')
AWS released a JavaScript library specifically for this purpose: https://github.com/awslabs/aws-jwt-verify.
The library has similar machinery to other libraries out there and mentioned here, such as automatically downloading, and caching, the JWKS (the public keys with which Cognito JWTs can be verified). It's written in pure TypeScript and has 0 dependencies.
import { CognitoJwtVerifier } from "aws-jwt-verify";
// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
userPoolId: "<user_pool_id>",
tokenUse: "access",
clientId: "<client_id>",
});
try {
const payload = await verifier.verify(
"eyJraWQeyJhdF9oYXNoIjoidk..." // the JWT as string
);
console.log("Token is valid. Payload:", payload);
} catch {
console.log("Token not valid!");
}
(By the way, the library also includes a class that works for other identity providers than Cognito)
Disclaimer: I'm one of the authors of the library. We're looking forward to customer feedback––do leave us a GitHub issue.
Short answer:
You can get the public key for your user pool from the following endpoint:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
If you successfully decode the token using this public key then the token is valid else it is forged.
Long answer:
After you successfully authenticate via cognito, you get your access and id tokens. Now you want to validate whether this token has been tampered with or not. Traditionally we would send these tokens back to the authentication service (which issued this token at the first place) to check if the token is valid. These systems use symmetric key encryption algorithms such as HMAC to encrypt the payload using a secret key and so only this system is capable to tell if this token is valid or not.
Traditional auth JWT token Header:
{
"alg": "HS256",
"typ": "JWT"
}
Note here that encryption algorithm used here is symmetric - HMAC + SHA256
But modern authentication systems like Cognito use asymmetric key encryption algorithms such as RSA to encrypt the payload using a pair of public and private key. Payload is encrypted using a private key but can be decoded via public key. Major advantage of using such an algorithm is that we don't have to request a single authentication service to tell if a token is valid or not. Since everyone has access to the public key, anyone can verify validity of token. The load for validation is fairly distributed and there is no single point of failure.
Cognito JWT token header:
{
"kid": "abcdefghijklmnopqrsexample=",
"alg": "RS256"
}
Asymmetric encryption algorithm used in this case - RSA + SHA256
cognito-jwt-verifier is a tiny npm package to verify ID and access JWT tokens obtained from AWS Cognito in your node/Lambda backend with minimal dependencies.
Disclaimer: I'm the author of this. I came up with it because I couldn't find anything checking all the boxes for me:
minimal dependencies
framework agnostic
JWKS (public keys) caching
test coverage
Usage (see github repo for a more detailed example):
const { verifierFactory } = require('#southlane/cognito-jwt-verifier')
const verifier = verifierFactory({
region: 'us-east-1',
userPoolId: 'us-east-1_PDsy6i0Bf',
appClientId: '5ra91i9p4trq42m2vnjs0pv06q',
tokenType: 'id', // either "access" or "id"
})
const token = 'eyJraWQiOiI0UFFoK0JaVE...' // clipped
try {
const tokenPayload = await verifier.verify(token)
} catch (e) {
// catch error and act accordingly, e.g. throw HTTP 401 error
}
I had a similar problem but without using the API Gateway. In my case I wanted to verify the signature of a JWT token obtained via the AWS Cognito Developer Authenticated identity route.
Like many posters on various sites I had trouble piecing together exactly the bits I needs to verify the signature of an AWS JWT token externally i.e., server side or via script
I think I figured out out and put a gist to verify an AWS JWT token signature. It'll verify an AWS JWT/JWS token with either pyjwt or PKCS1_v1_5c from Crypto.Signature in PyCrypto
So, yes this was python in my case but it's also doable easily in node (npm install jsonwebtoken jwk-to-pem request).
I attempted to highlight some gotchas in the comments because when I was trying to figure this out I was mostly doing the right thing but there were some nuances like python dict ordering, or lack there of, and json representation.
Hopefully it may help somebody somewhere.
You can get insights from the Lambda code here
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt
In Golang
https://gist.github.com/tmaiaroto/e2ee5e88fc6ae035307d7c5ee71a99cf
this is working for me in dot net 4.5
public static bool VerifyCognitoJwt(string accessToken)
{
string[] parts = accessToken.Split('.');
string header = parts[0];
string payload = parts[1];
string headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
JObject headerData = JObject.Parse(headerJson);
string payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
JObject payloadData = JObject.Parse(payloadJson);
var kid = headerData["kid"];
var iss = payloadData["iss"];
var issUrl = iss + "/.well-known/jwks.json";
var keysJson= string.Empty;
using (WebClient wc = new WebClient())
{
keysJson = wc.DownloadString(issUrl);
}
var keyData = GetKeyData(keysJson,kid.ToString());
if (keyData==null)
throw new ApplicationException(string.Format("Invalid signature"));
var modulus = Base64UrlDecode(keyData.Modulus);
var exponent = Base64UrlDecode(keyData.Exponent);
RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
var rsaParameters= new RSAParameters();
rsaParameters.Modulus = new BigInteger(modulus).ToByteArrayUnsigned();
rsaParameters.Exponent = new BigInteger(exponent).ToByteArrayUnsigned();
provider.ImportParameters(rsaParameters);
SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(provider);
rsaDeformatter.SetHashAlgorithm(sha256.GetType().FullName);
if (!rsaDeformatter.VerifySignature(hash, Base64UrlDecode(parts[2])))
throw new ApplicationException(string.Format("Invalid signature"));
return true;
}
public class KeyData
{
public string Modulus { get; set; }
public string Exponent { get; set; }
}
private static KeyData GetKeyData(string keys,string kid)
{
var keyData = new KeyData();
dynamic obj = JObject.Parse(keys);
var results = obj.keys;
bool found = false;
foreach (var key in results)
{
if (found)
break;
if (key.kid == kid)
{
keyData.Modulus = key.n;
keyData.Exponent = key.e;
found = true;
}
}
return keyData;
}
Someone also wrote a python package called cognitojwt that works in both async/sync mode to decode and verify Amazon Cognito JWT.
This is based on the elaborate explanation from Derek (answer). I have been able to create a working sample for PHP.
I have used https://github.com/firebase/php-jwt for pem creation and code verification.
This code is used after you received a set of base64 encoded tokens.
<?php
require_once(__DIR__ . '/vendor/autoload.php');
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
function debugmsg($msg, $output) {
print_r($msg . "\n");
}
$tokensReceived = array(
'id_token' => '...',
'access_token' => '...',
'refresh_token' => '...',
'expires_in' => 3600,
'token_type' => 'Bearer'
);
$idToken = $tokensReceived['id_token'];
// 'https://cognito-idp.us-west-2.amazonaws.com/<pool-id>/.well-known/jwks.json'
$keys = json_decode('<json string received from jwks.json>');
$idTokenHeader = json_decode(base64_decode(explode('.', $idToken)[0]), true);
print_r($idTokenHeader);
$remoteKey = null;
$keySets = JWK::parseKeySet($keys);
$remoteKey = $keySets[$idTokenHeader['kid']];
try {
print_r("result: ");
$decoded = JWT::decode($idToken, $remoteKey, array($idTokenHeader['alg']));
print_r($decoded);
} catch(Firebase\JWT\ExpiredException $e) {
debugmsg("ExpiredException","cognito");
} catch(Firebase\JWT\SignatureInvalidException $e) {
debugmsg("SignatureInvalidException","cognito");
} catch(Firebase\JWT\BeforeValidException $e) {
debugmsg("BeforeValidException","cognito");
}
?>
Related
In OpenIddict, is it possible to have a second token endpoint that requires authorization and returns a long-lived token?
I'm converting a .Net Framework application to Core. As part of that I'm trying to swap the OAuth portions to OpenIddict. I've got standard authentication working using token endpoint just fine.
What I've been unable to do, or find an example of, is a second authenticated endpoint that generates a different token.
The purpose of the second endpoint is to provide a token similar to the PAT you get from GitHub or Azure DevOps
I was able to use this code to create a token on a second endpoint, but was not valid for authentication as I could not register it with OpenIddidct
var options = _oidcOptions.CurrentValue;
var descriptor = new SecurityTokenDescriptor
{
Claims = new Dictionary<string, object>
{
{ "sub", "your user id" },
{ "scope", "your scopes" },
},
EncryptingCredentials = options.DisableAccessTokenEncryption
? null
: options.EncryptionCredentials.First(),
Expires = null, // recommended to set this
IssuedAt = DateTime.UtcNow,
Issuer = "https://contoso.com/", // the URL your auth server is hosted on, with trailing slash
SigningCredentials = options.SigningCredentials.First(),
TokenType = OpenIddictConstants.JsonWebTokenTypes.AccessToken,
};
var accessToken = options.JsonWebTokenHandler.CreateToken(descriptor);
There are a number of solutions to this:
use the build-in dialog provided by esri/IdentityManager (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html)
use a server-side proxy (https://github.com/Esri/resource-proxy)
use the identity manager initialize() method (https://developers.arcgis.com/javascript/3/jsapi/identitymanagerbase-amd.html#initialize)
But there what is missing is the ability to hook into the request for a token. I am working with ArcGISDynamicMapServiceLayer and there is no way to know if the server return a 498/499, and no way to update the url to update the token.
I started hacking around in the API to try to hook into various events with no real promise of success. What seems to be missing:
a way to detect when a token is needed
a way to update the token
Closes I came up with is listening for "dialog-create" but there is no way to disable the dialog apart from throwing an exception, which disables the layer.
I tried replacing the "_createLoginDialog" method and returning {open: true} as a trick to pause the layers until I had a token ready but since there is no way to update the layer endpoint I did not pursue this hack. It seems the only way this might work is to use the initialize() method on the identity manager.
Does anyone have knowledge of options beyond what I have outlined?
EDIT: The goal is to provide a single-sign-on experience to users of our product.
"User" is already signed in to our application
"User" wishes to access a secure ESRI ArcGIS Server MapServer or FeatureServer services from the ESRI JSAPI
"User" is prompted for user name and password
The desired flow is to acquire a token on the users behalf using a RESTful services in our product and return the appropriate token that will allow the "User" to access the secure services without being prompted.
I do not wish to use a proxy because I do not want all that traffic routed through the proxy.
I do not wish to use initialize() because it is complicated and not clear how that works apart for re-hydrating the credentials.
I do wish for an API that simply allows me to set the token on any layer services that report a 499 (missing token) or 498 (invalid token), but I cannot find any such API. The solution I am focusing on hinges on being able to update the url of an ArcGISImageServiceLayer instance with a new token.
This answer lacks in satisfaction but delivers on my requirements. I will start with the code (client-side typescript):
class TokenProxy {
private tokenAssuranceHash = {} as Dictionary<Promise<{ token: string, expiration: string }>>;
private service = new TokenService();
private timeoutHandle = 0;
watchLayer(esriLayer: ArcGISDynamicMapServiceLayer) {
setInterval(async () => {
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
}, 5000);
}
updateRefreshInterval(ticks: number) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = setTimeout(() => {
Object.keys(this.tokenAssuranceHash).forEach(url => {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
});
this.updateRefreshInterval(ticks);
}, ticks);
}
async tokenAssurance(url: string) {
if (!this.tokenAssuranceHash[url]) {
this.tokenAssuranceHash[url] = this.service.getMapToken({serviceUrl: url});
}
try {
const response = await this.tokenAssuranceHash[url];
await this.recomputeRefreshInterval();
return response.token;
} catch (ex) {
console.error(ex, "could not acquire token");
return null;
}
}
async recomputeRefreshInterval() {
const keys = Object.keys(this.tokenAssuranceHash);
if (!keys.length) return;
const values = keys.map(k => this.tokenAssuranceHash[k]);
const tokens = await Promise.all(values);
const min = Math.min(...tokens.map(t => new Date(t.expiration).getTime()));
if (Number.isNaN(min)) return; // error occured, do not update the refresh interval
const nextRefreshInTicks = min - new Date().getTime();
this.updateRefreshInterval(0.90 * nextRefreshInTicks);
}
}
And highlight the hack that makes it work:
const key = esriLayer._url.path;
const token = await this.tokenAssurance(key);
esriLayer._url.query.token = token;
The "_url" is a hidden/private model that I should not be using to update the token but it works.
I have an api that is protected by JWT and Authorize attribute and at the client I use jquery ajax call to deal with it.
This works fine, however I now need to be able to secure downloading of files so I can't set a header Bearer value, can it be done in the URI as an url parameter?
=-=-=-=-
UPDATE: This is what I ended up doing for my scenario which is an in-house project and very low volume but security is important and it might need to scale in future:
When user logs in I generate a random download key and put it in their user record in the db along with the expiry date of their JWT and return the download key to the client. The download route is protected to only allow a download if there is a query parameter that has the download key and that key exists in the user records and that expiry date has not passed. This way the dl key is unique per user, valid as long as the user's auth session is valid and can be revoked easily.
This is a common problem.
Whenever you want to reference images or other files directly from an API in a single page application's HTML, there isn't a way to inject the Authorization request header between the <img> or <a> element and the request to the API. You can sidestep this by using some fairly new browser features as described here, but you may need to support browsers that lack this functionality.
Fortunately, RFC 6750 specifies a way to do exactly what you're asking via the "URI Query Parameter" authentication approach. If you follow its convention, you would accept JWTs using the following format:
https://server.example.com/resource?access_token=mF_9.B5f-4.1JqM&p=q
As stated in another answer and in RFC 6750 itself, you should be doing this only when necessary. From the RFC:
Because of the security weaknesses associated with the URI method (see Section 5), including the high likelihood that the URL containing the access token will be logged, it SHOULD NOT be used unless it is impossible to transport the access token in the "Authorization" request header field or the HTTP request entity-body.
If you still decide to implement "URI Query Parameter" authentication, you can use the Invio.Extensions.Authentication.JwtBearer library and call AddQueryStringAuthentication() extension method on JwtBearerOptions. Or, if you want to do it manually, you can certainly do that as well. Here's a code sample that shows both ways as extensions of the Microsoft.AspNetCore.Authentication.JwtBearer library.
public void ConfigureServices(IServiceCollection services) {
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
options => {
var authentication = this.configuration.GetSection("Authentication");
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuers = authentication["Issuer"],
ValidAudience = authentication["ClientId"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authentication["ClientSecret"])
)
};
// OPTION 1: use `Invio.Extensions.Authentication.JwtBearer`
options.AddQueryStringAuthentication();
// OPTION 2: do it manually
options.Events = new JwtBearerEvents {
OnMessageReceived = (context) => {
StringValues values;
if (!context.Request.Query.TryGetValue("access_token", out values)) {
return Task.CompletedTask;
}
if (values.Count > 1) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"Only one 'access_token' query string parameter can be defined. " +
$"However, {values.Count:N0} were included in the request."
);
return Task.CompletedTask;
}
var token = values.Single();
if (String.IsNullOrWhiteSpace(token)) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"The 'access_token' query string parameter was defined, " +
"but a value to represent the token was not included."
);
return Task.CompletedTask;
}
context.Token = token;
return Task.CompletedTask;
}
};
}
);
}
You can use a middleware to set the authorization header from the query param:
public class SecureDownloadUrlsMiddleware
{
private readonly RequestDelegate next;
public SecureDownloadUrlsMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
// get the token from query param
var token = context.Request.Query["t"];
// set the authorization header only if it is empty
if (string.IsNullOrEmpty(context.Request.Headers["Authorization"]) &&
!string.IsNullOrEmpty(token))
{
context.Request.Headers["Authorization"] = $"Bearer {token}";
}
await next(context);
}
}
and then in Startup.cs use the middleware before the authentication middleware:
app.UseMiddleware(typeof(SecureDownloadUrlsMiddleware));
app.UseAuthentication();
Although it is technically possible to include a JWT in the URL, it is strongly discouraged. See the quote from here, which explains why it's a bad idea:
Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be
passed in page URLs (for example, as query string parameters).
Instead, bearer tokens SHOULD be passed in HTTP message headers or
message bodies for which confidentiality measures are taken. Browsers,
web servers, and other software may not adequately secure URLs in the
browser history, web server logs, and other data structures. If bearer
tokens are passed in page URLs, attackers might be able to steal them
from the history data, logs, or other unsecured locations.
However, if you have no choice or just don't care about security practices, see Technetium's answer.
If you still need it,you have to set jwt token on localStorage.After,you have to create a new header with the following code:
'functionName'():Headers{
let header =new Headers();
let token = localStorage.getItem('token')
header.append('Authorization',`Bearer ${token}`);
return header;
}
Add Hader to http requests.
return this.http.get('url',new RequestOptions({headers:this.'serviceName'.'functionName'()}))
Although this is a bit outside of the box, I would advice you to do the same as this is the best scalable solution when developing in the .NET environment.
Use Azure Storage! Or any other similar online cloud storage solution.
It makes sure your web app is separate from your files, so you don't have to worry about moving an application to a different web environment.
Web storage is mostly more expensive then azure storage (1GB with about 3000 operations (read/write/list) costs in total about $0.03.
When you scale your application where downtime is more critical, point 1 also applies when you use a swapping/staging technique.
Azure storage takes care of the expiry of so called Shared Access Tokens (SAS)
For the sake of simplicity for you, I will just include my code here so you don't have to google the rest
So what I do in my case, all my files are saved as Attachments within the database (not the actual file of course).
When someone requests an attachment, I do a quick check to see if the expire date has passed and if so we should generate a new url.
//where ever you want this to happen, in the controller before going to the client for example
private async Task CheckSasExpire(IEnumerable<AttachmentModel> attachments)
{
foreach (AttachmentModel attachment in attachments)
{
await CheckSasExpire(attachment);
}
}
private async Task CheckSasExpire(AttachmentModel attachment)
{
if (attachment != null && attachment.LinkExpireDate < DateTimeOffset.UtcNow && !string.IsNullOrWhiteSpace(attachment.AzureContainer))
{
Enum.TryParse(attachment.AzureContainer, out AzureStorage.ContainerEnum container);
string url = await _azureStorage.GetFileSasLocator(attachment.Filename, container);
attachment.FileUrl = url;
attachment.LinkExpireDate = DateTimeOffset.UtcNow.AddHours(1);
await _attachmentRepository.UpdateAsync(attachment.AttachmentId, attachment);
}
}
AzureStorage.ContainerEnum is just an internal enum to easily track the container certain files are stored in, but these can be strings of course
And my AzureStorage class:
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public async Task<string> GetFileSasLocator(string filename, ContainerEnum container, DateTimeOffset expire = default(DateTimeOffset))
{
var cont = await GetContainer(container);
CloudBlockBlob blockBlob = cont.GetBlockBlobReference(filename);
DateTimeOffset expireDate = DateTimeOffset.UtcNow.AddHours(1);//default
if (expire != default(DateTimeOffset) && expire > expireDate)
{
expireDate = expire.ToUniversalTime();
}
SharedAccessBlobPermissions permission = SharedAccessBlobPermissions.Read;
var sasConstraints = new SharedAccessBlobPolicy
{
SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-30),
SharedAccessExpiryTime = expireDate,
Permissions = permission
};
var sasToken = blockBlob.GetSharedAccessSignature(sasConstraints);
return blockBlob.Uri + sasToken;
}
private async Task<CloudBlobContainer> GetContainer(ContainerEnum container)
{
//CloudConfigurationManager.GetSetting("StorageConnectionString")
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_config["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
string containerName = container.ToString().ToLower();
CloudBlobContainer cloudContainer = blobClient.GetContainerReference(containerName);
await cloudContainer.CreateIfNotExistsAsync();
return cloudContainer;
}
So this will produce url's like so: http://127.0.0.1:10000/devstoreaccount1/invoices/NL3_2002%20-%202019-04-12.pdf?sv=2018-03-28&sr=b&sig=gSiohA%2BGwHj09S45j2Deh%2B1UYP1RW1Fx5VGeseNZmek%3D&st=2019-04-18T14%3A16%3A55Z&se=2019-04-18T15%3A46%3A55Z&sp=r
Of course you have to apply your own authentication logic when retrieving the attachments, if the user is allowed to view the file or not. But that can all be done with the JWT token and in the controller or the repository. I wouldn't worry about the URL being a public url, if one is so mighty to get that URL... within one hour... well then reduce the expire date :D
I'm trying out adal.js with an Angular SPA (Single Page Application) web site that gets data from an external Web API site (different domain). Authentication against the SPA was easy with adal.js, but getting it to communicate with the API is not working at all when bearer tokens are required. I have used https://github.com/AzureAD/azure-activedirectory-library-for-js as template in addition to countless blogs.
The problem is that when I set up endpoints while initiating adal.js, adal.js seems to redirect all outgoing endpoint traffic to microsofts login service.
Observations:
Adal.js session storage contains two adal.access.token.key entries. One for the client ID of the SPA Azure AD application and one for the external api. Only the SPA token has a value.
If I do not inject $httpProvider into adal.js, then calls go out to the external API and I get a 401 in return.
If I manually add the SPA token to the http header ( authorization: bearer 'token value') I get a 401 in return.
My theory is that adal.js is unable to retrieve tokens for endpoints (probably because I configured something wrong in the SPA) and it stops traffic to the endpoint since it is unable to get a required token. The SPA token cannot be used against the API since it does not contain the required rights. Why is adal.js not getting tokens for endpoints and how can I fix it?
Additional information:
The client Azure AD application is configured to use delegated permissions against the API and oauth2AllowImplicitFlow = true in app manifest.
The API Azure AD application is configured for impersonation and oauth2AllowImplicitFlow = true (do not think that is required, but tried it). It is multi tenant.
The API is configured to allow all CORS origins and it works correctly when used by another web app using impersonation (hybrid MVC (Adal.net) + Angular).
Session storage:
key (for the SPA application): adal.access.token.keyxxxxx-b7ab-4d1c-8cc8-xxx value: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1u...
key (for API application): adal.access.token.keyxxxxx-bae6-4760-b434-xxx
value:
app.js (Angular and adal configuration file)
(function () {
'use strict';
var app = angular.module('app', [
// Angular modules
'ngRoute',
// Custom modules
// 3rd Party Modules
'AdalAngular'
]);
app.config(['$routeProvider', '$locationProvider',
function ($routeProvider, $locationProvider) {
$routeProvider
// route for the home page
.when('/home', {
templateUrl: 'App/Features/Test1/home.html',
controller: 'home'
})
// route for the about page
.when('/about', {
templateUrl: 'App/Features/Test2/about.html',
controller: 'about',
requireADLogin: true
})
.otherwise({
redirectTo: '/home'
})
//$locationProvider.html5Mode(true).hashPrefix('!');
}]);
app.config(['$httpProvider', 'adalAuthenticationServiceProvider',
function ($httpProvider, adalAuthenticationServiceProvider) {
// endpoint to resource mapping(optional)
var endpoints = {
"https://localhost/Api/": "xxx-bae6-4760-b434-xxx",
};
adalAuthenticationServiceProvider.init(
{
// Config to specify endpoints and similar for your app
clientId: "xxx-b7ab-4d1c-8cc8-xxx", // Required
//localLoginUrl: "/login", // optional
//redirectUri : "your site", optional
extraQueryParameter: 'domain_hint=mydomain.com',
endpoints: endpoints // If you need to send CORS api requests.
},
$httpProvider // pass http provider to inject request interceptor to attach tokens
);
}]);
})();
Angular code for calling endpoint:
$scope.getItems = function () {
$http.get("https://localhost/Api/Items")
.then(function (response) {
$scope.items = response.Items;
});
Ok, I've been bashing my head against the wall to figure this out. Trying to make my ADAL.js SPA app (sans angular) successfully make cross-domain XHR requests over to my precious CORS-enabled Web API.
This sample app, the one all the newbies like me are using, has this problem: it features an API and SPA all served from the same domain - and only requires a single AD Tenant app registration. This only confuses things when it comes time to pull things apart into separate pieces.
So, out of the box, the sample has this Startup.Auth.cs which works OK, as far as the sample goes...
public void ConfigureAuth(IAppBuilder app) {
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
});
}
but, you need to modify the above code, drop the Audience assignment, and go for an array of audiences.. That's right: ValidAudiences .. So, for every SPA client that is talking to your WebAPI, you'll want to put the ClientID of your SPA registration in this array...
It should look like this...
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new [] {
ConfigurationManager.AppSettings["ida:Audience"],//my swagger SPA needs this 1st one
"b2d89382-f4d9-42b6-978b-fabbc8890276",//SPA ClientID 1
"e5f9a1d8-0b4b-419c-b7d4-fc5df096d721" //SPA ClientID 2
},
RoleClaimType = "roles" //Req'd only if you're doing RBAC
//i.e. web api manifest has "appRoles"
}
});
}
EDIT
Ok, based on #JonathanRupp's feedback, I was able to reverse out the Web API solution I was using shown above, and was able to modify my client JavaScript as shown below to make everything work.
// Acquire Token for Backend
authContext.acquireToken("https://mycorp.net/WebApi.MyCorp.RsrcID_01", function (error, token) {
// Handle ADAL Error
if (error || !token) {
printErrorMessage('ADAL Error Occurred: ' + error);
return;
}
// Get TodoList Data
$.ajax({
type: "GET",
crossDomain: true,
headers: {
'Authorization': 'Bearer ' + token
},
url: "https://api.mycorp.net/odata/ToDoItems",
}).done(function (data) {
// For Each Todo Item Returned, do something
var output = data.value.reduce(function (rows, todoItem, index, todos) {
//omitted
}, '');
// Update the UI
//omitted
}).fail(function () {
//do something with error
}).always(function () {
//final UI cleanup
});
});
ADAL.js does get the access_token apart from id_token for calling Azure AD protected API running on different domain.
Initially, during login, it only takes id_token. This token has the access for accessing resource of the same domain.
But, on calling the API running in different domain, adal interceptor checks if the API URL is configured in as endpoint in adal.init().
It is only then that the access token is called for the requested resource. It also necessitates that the SPA is configured in the AAD to access API APP.
The key to achieve this is following:
1. Add endpoints in the adal.init()
var endpoints = {
// Map the location of a request to an API to a the identifier of the associated resource
//"Enter the root location of your API app here, e.g. https://contosotogo.azurewebsites.net/":
// "Enter the App ID URI of your API app here, e.g. https://contoso.onmicrosoft.com/TestAPI",
"https://api.powerbi.com": "https://analysis.windows.net/powerbi/api",
"https://localhost:44300/": "https://testpowerbirm.onmicrosoft.com/PowerBICustomServiceAPIApp"
};
adalProvider.init(
{
instance: 'https://login.microsoftonline.com/',
tenant: 'common',
clientId: '2313d50b-7ce9-4c0e-a142-ce751a295175',
extraQueryParameter: 'nux=1',
endpoints: endpoints,
requireADLogin: true,
//cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
// Also, token acquisition for the To Go API will fail in IE when running on localhost, due to IE security restrictions.
},
$httpProvider
);
Give permission to the SPA application in Azure AD to access the API application:
You may refer this link for details : ADAL.js deep dive
You need to make your Web API aware of your Client application. It's not enough to add delegated permission to API from your Client.
To make the API client aware, go to Azure management portal, download API's manifest and add ClientID of your Client application to the list of "knownClientApplications".
To allow Implicit flow you need to set "oauth2AllowImplicitFlow" to true in the manifest as well.
Upload the manifest back to API application.
I'm not sure if our setup is exactly the same, but I think it it comparable.
I have a Angular SPA that uses and external Web API through Azure API Management (APIM). My code might not be best practice, but it works for me so far :)
The SPAs Azure AD app has a delegated permission to access the External APIs Azure AD app.
The SPA (is based upon the Adal TodoList SPA sample)
app.js
adalProvider.init(
{
instance: 'https://login.microsoftonline.com/',
tenant: 'mysecrettenant.onmicrosoft.com',
clientId: '********-****-****-****-**********',//ClientId of the Azure AD app for my SPA app
extraQueryParameter: 'nux=1',
cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
},
$httpProvider
);
Snippet from the todoListSvc.js
getWhoAmIBackend: function () {
return $http.get('/api/Employee/GetWhoAmIBackend');
},
Snippets from the EmployeeController
public string GetWhoAmIBackend()
{
try
{
AuthenticationResult result = GetAuthenticated();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var request = new HttpRequestMessage()
{
RequestUri = new Uri(string.Format("{0}", "https://api.mydomain.com/secretapi/api/Employees/GetWhoAmI")),
Method = HttpMethod.Get, //This is the URL to my APIM endpoint, but you should be able to use a direct link to your external API
};
request.Headers.Add("Ocp-Apim-Trace", "true"); //Not needed if you don't use APIM
request.Headers.Add("Ocp-Apim-Subscription-Key", "******mysecret subscriptionkey****"); //Not needed if you don't use APIM
var response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
var res = response.Content.ReadAsStringAsync().Result;
return res;
}
return "No dice :(";
}
catch (Exception e)
{
if (e.InnerException != null)
throw e.InnerException;
throw e;
}
}
private static AuthenticationResult GetAuthenticated()
{
BootstrapContext bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as BootstrapContext;
var token = bootstrapContext.Token;
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext =
new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/mysecrettenant.onmicrosoft.com");
//The Client here is the SPA in Azure AD. The first param is the ClientId and the second is a key created in the Azure Portal for the AD App
ClientCredential credential = new ClientCredential("clientid****-****", "secretkey ********-****");
//Get username from Claims
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
//Creating UserAssertion used for the "On-Behalf-Of" flow
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
//Getting the token to talk to the external API
var result = authContext.AcquireToken("https://mysecrettenant.onmicrosoft.com/backendAPI", credential, userAssertion);
return result;
}
Now, in my backend external API, my Startup.Auth.cs looks like this:
The external API
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
SaveSigninToken = true
},
AuthenticationType = "OAuth2Bearer"
});
}
Please let me know if this helps or if I can be of further assistance.
I am currently working on an interaction between Angular JS app and Node.js Server (as API) with an authentication based on JSON Web Token.
But I have a question I can't answer by myself : when you encode the JWT server-side putting a user as payload, how do you proceed to retrieve the user information client-side ?
Here is a small example to understand my question:
I am a basic user, I send my credentials to the API for authenticating. In exchange, I receive a JWT token but I don't have any information about the user since only the server has the secret key that is able to decode the JWT token. So does the server need to send me for example the id of the user so that I could call my api user/id for retrieving information about the user authenticated?
You retrieve the user's info by decoding the token on each request. So in your example after the token is returned to the client, the client makes a request to the server to grab the user's first and last name using the data stored in the encoded token which is sent along with the request back to the server. When making this GET request, you can send the token as a parameter. I'll use a non-cookie stored example. Here's how it goes down:
The user signs in with their password and username
The server encodes a json web token payload that contains the unique identifier (i.e. user_id) of the user that signed in using the secret_key. An example function call may look something like this.
payload = {user_id: 35}
user_token = JWT.encode(payload, "your_secret_key");
Return the user_token to the client and store said token in a hidden html tag or in a localStorage variable. Using Angular, I'd store it in localStorage.
Now that the user is signed_in and the token is client-side, you can submit a GET request that contains the user_token as a parameter. Remember, this user_token payload contains the user_id.
The server gets the parameter and decodes the user_token to get the user_id from the payload.
You query the database with the user_id and return the data (first and last name) as plain json, NOT ENCODED.
It's important to remember the only thing to encode in your example is the unique identifier (user_id). On each request you decode the token which itself is the authentication mechanism.
You have the payload on the client, If your needed data is in the payload you can easily do a Base64 Decode on payload to find it!
To understand this here are steps:
Client send username:user,password:pass to server.
The server starts the authentication business and finds that the user name and password is valid.
The server must return these information back to client. Here is where JWT has some rules. The server must return a token back to client. The token has three parts Header.PayLoad.Signature . Forget about signature right now, which is the part which make some confusion.
The part one is Header. Some thing like:
{"typ":"JWT","alg":"HS256"}
Which will be eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 after Base64 Decode. Please consider this is just a decode, no encryption at all! To see this you can go to https://www.base64decode.org/ and test.
After the header, the server needs to send a payload to user. The server may decide to send below json ( I said decide, because there is no standard requirement here, you can send more or less data as payload, for example, you may also set user privileges for example admin:true, or user first and last name, but keep in mind that the JWT size must be small as it will be send to server on each request)
{"username":"user","id":3,"iat":1465032622,"exp":1465050622}
Again according to JWT, the server needs a Base64 Decode here ( and again no encryption at all). The above json will be eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9.
Until now the server created the Header and Payload. Now time to make signature! It is very easy:
var encodedString=base64UrlEncode(header) + "." + base64UrlEncode(payload);
//As our example base64UrlEncode(header) is eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
//and the base64UrlEncode(payload) is eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9
var signature=HMACSHA256(encodedString, 'a secret string which is kept at server');
The signature is made with a secret key which you don't have it at clent!! You don't need it either. All token data is in the payload and can be accessed with decode ( again no decrypt ! ).
This signature is used at the server, when you send token back to server, the server check that signiature is correct to make sure he can trust the token data.
To summarize have a look at below token
//Header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//PayLoad
eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9.
//Signature
0K8TL1YS0XKnEIfI3lYs-bu2vbWHSNZsVJkN1mXtgWg
Header and payloads are Base64 Decoded and you can encode it on client. But you can not do any thing with signature.
The signature is only used by the server. The client send each request with his token, the server must be sure that the client did not change any part of token payload (for example change userid). This is where the signature string come importance is revealed, the server recheck the signature with it's secret key for every request!
Note:
Do you still wonder why the JWT use encode and decode ?! To make the hole token URL safe !
The strategy in the accepted answer works, but it misses the fact that the client can see the payload of a JWT. It is explained nicely in The Anatomy of a JSON Web Token.
A JWT has 3 parts. The first two, header and payload, are base64 encoded. The client can decode them easily. The payload has claims about the user, the client can use this data (user id, name, roles, token expiration) w/out having to make another request to the server.
The third part of the JWT is the signature. It is a hash of the header, the payload, and a secret that only the server knows. The server will validate the token and user's permissions on every request.
The client never knows the secret, it just has a token that claims to be the given user.
JWT (JSON web token) has become more and more popular in web development. It is
an open standard which allows transmitting data between parties as a JSON object in a secure and compact way. The data transmitting using JWT between parties are digitally signed so that it can be easily verified and trusted.
JWT in ASP.NET Core
The first step is to configure JWT based authentication in our project. we can add custom jwt auth middleware that fire in every request for Authorization.
Startup.cs
services.AddMvc(options => options.EnableEndpointRouting = false);
var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey("Jwt_Key"),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
RequireExpirationTime = false,
ValidIssuer = "Jwt_Issuer",
ValidAudience = "Jwt_Audience",
ClockSkew = TimeSpan.Zero
};
services.AddSingleton(tokenValidationParams);
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => {
jwt.SaveToken = true;
jwt.TokenValidationParameters = tokenValidationParams;
});
services.AddMvc();
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// custom jwt auth middleware
**app.UseMiddleware<JwtMiddleware>();**
app.UseAuthentication();
app.UseMvc();
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome to DATA API");
});
}
Generete JWT
GenerateJSONWebToken(User userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Jwt_Key"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserID),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken("Jwt_Issuer","Jwt:Audience",
claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
This Method return JWT Totken like
Token : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKa
WduZXNoIFRyaXZlZGkiLCJlbWFpbCI6InRlc3QuYnRlc3RAZ21haWwuY29tIiwiRG
F0ZU9mSm9pbmciOiIwMDAxLTAxLTAxIiwianRpIjoiYzJkNTZjNzQtZTc3Yy00ZmU
xLTgyYzAtMzlhYjhmNzFmYzUzIiwiZXhwIjoxNTMyMzU2NjY5LCJpc3MiOiJUZXN0
LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.8hwQ3H9V8mdNYrFZSjbCpWSyR1CNyDYHc
Gf6GqqCGnY"
Calling Authorize Method
[Authorize]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2", "value3", "value4",
"value5" };
}
Validate Token in Jwt Middleware Class
JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenValidationParameters _tokenValidationParams;
public JwtMiddleware(RequestDelegate next, TokenValidationParameters
tokenValidationParams)
{
_next = next;
_tokenValidationParams = tokenValidationParams;
}
public async Task Invoke(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
var jwtTokenHandler = new JwtSecurityTokenHandler();
// Validation 1 - Validation JWT token format
var tokenInVerification = jwtTokenHandler.ValidateToken(token, _tokenValidationParams, out var validatedToken);
if (validatedToken is JwtSecurityToken jwtSecurityToken)
{
var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);
if (result == false)
{
Error Invalid = new Error()
{
Success = false,
Errors = "Token is Invalid"
};
context.Items["Error"] = Invalid;
}
}
await _next(context);
}
}
Authorize Attribute
public void OnAuthorization(AuthorizationFilterContext context)
{
var Error= (UserModel)context.HttpContext.Items["Error"];
if (AuthResult != null)
{
// not logged in
context.Result = new JsonResult(new { message = "Unauthorized Access" }) {
StatusCode = StatusCodes.Status401Unauthorized };
}
}
I hope this will work for you.