Dans AppSync, lorsque vous utilisez des groupes d’utilisateurs Cognito comme paramètres d’authentification, vous obtenez
identity:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
issuer: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
username: 'skillet',
claims:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
aud: '7re1oap5fhm3ngpje9r81vgpoe',
email_verified: true,
event_id: 'bb65ba5d-4689-11e8-bee7-2d0da8da81ab',
token_use: 'id',
auth_time: 1524441800,
iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
'cognito:username': 'skillet',
exp: 1524459387,
iat: 1524455787,
email: '[email protected]' },
sourceIp: [ '11.222.33.200' ],
defaultAuthStrategy: 'ALLOW',
groups: null }
Cependant, lorsque vous utilisez AWS_IAM auth, vous obtenez
identity:
{ accountId: '12121212121', //<--- my Amazon account ID
cognitoIdentityPoolId: 'us-west-2:39b1f3e4-330e-40f6-b738-266682302b59',
cognitoIdentityId: 'us-west-2:a458498b-b1ac-46c1-9c5e-bf932bad0d95',
sourceIp: [ '33.222.11.200' ],
username: 'AROAJGBZT5A433EVW6O3Q:CognitoIdentityCredentials',
userArn: 'arn:aws:sts::454227793445:assumed-role/MEMORYCARDS-CognitoAuthorizedRole-dev/CognitoIdentityCredentials',
cognitoIdentityAuthType: 'authenticated',
cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7"' }
Docs indique que cela est prévu, https://docs.aws.Amazon.com/appsync/latest/devguide/resolver-context-reference.html . Toutefois, si vous utilisez AWS_IAM
connecté à Cognito (ce qui est nécessaire pour avoir un accès non authentifié), comment êtes-vous censé obtenir le nom d'utilisateur, l'email, le sous, etc. de l'utilisateur? J'ai besoin d'accéder aux revendications de l'utilisateur lors de l'utilisation de AWS_IAM
, tapez Auth.
Voici ma réponse. Il y avait un bogue dans la bibliothèque cliente appSync qui écrasait tous les en-têtes personnalisés. Cela a été corrigé depuis. Vous pouvez maintenant transmettre des en-têtes personnalisés qui vous conduiront jusqu’à vos résolveurs, que je transmettrai à mes fonctions lambda (encore une fois, notez que j’utilise datasourcres lambda et que je n’utilise pas dynamoDB).
Donc, je rattache mon JWT connecté côté client et, côté serveur dans ma fonction lambda, je le décode. Vous avez besoin de la clé publique créée par cognito pour valider le JWT. (VOUS N'AVEZ PAS BESOIN DE CLÉ SECRET.) Il existe une URL de "clé bien connue" associée à chaque groupe d'utilisateurs auquel je cingle lors de la première utilisation de mon lambda mais, tout comme ma connexion mongoDB, elle persiste entre les appels lambda ( au moins pour un moment.)
Voici le résolveur lambda ...
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise-native');
const _ = require('lodash')
//ITEMS THAT SHOULD BE PERSISTED BETWEEN LAMBDA EXECUTIONS
let conn = null; //MONGODB CONNECTION
let pem = null; //PROCESSED JWT PUBLIC KEY FOR OUR COGNITO USER POOL, SAME FOR EVERY USER
exports.graphqlHandler = async (event, lambdaContext) => {
// Make sure to add this so you can re-use `conn` between function calls.
// See https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas
lambdaContext.callbackWaitsForEmptyEventLoop = false;
try{
////////////////// AUTHORIZATION/USER INFO /////////////////////////
//ADD USER INFO, IF A LOGGED IN USER WITH VALID JWT MAKES THE REQUEST
var token = _.get(event,'context.request.headers.jwt'); //equivalen to "token = event.context.re; quest.headers.alexauthorization;" but fails gracefully
if(token){
//GET THE ID OF THE PUBLIC KEY (KID) FROM THE TOKEN HEADER
var decodedToken = jwt.decode(token, {complete: true});
// GET THE PUBLIC KEY TO NEEDED TO VERIFY THE SIGNATURE (no private/secret key needed)
if(!pem){
await request({ //blocking, waits for public key if you don't already have it
uri:`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
resolveWithFullResponse: true //Otherwise only the responce body would be returned
})
.then(function ( resp) {
if(resp.statusCode != 200){
throw new Error(resp.statusCode,`Request of JWT key with unexpected statusCode: expecting 200, received ${resp.statusCode}`);
}
let {body} = resp; //GET THE REPSONCE BODY
body = JSON.parse(body); //body is a string, convert it to JSON
// body is an array of more than one JW keys. User the key id in the JWT header to select the correct key object
var keyObject = _.find(body.keys,{"kid":decodedToken.header.kid});
pem = jwkToPem(keyObject);//convert jwk to pem
});
}
//VERIFY THE JWT SIGNATURE. IF THE SIGNATURE IS VALID, THEN ADD THE JWT TO THE IDENTITY OBJECT.
jwt.verify(token, pem, function(error, decoded) {//not async
if(error){
console.error(error);
throw new Error(401,error);
}
event.context.identity.user=decoded;
});
}
return run(event)
} catch (error) {//catch all errors and return them in an orderly manner
console.error(error);
throw new Error(error);
}
};
//async/await keywords used for asynchronous calls to prevent lambda function from returning before mongodb interactions return
async function run(event) {
// `conn` is in the global scope, Lambda may retain it between function calls thanks to `callbackWaitsForEmptyEventLoop`.
if (conn == null) {
//connect asyncoronously to mongodb
conn = await mongoose.createConnection(process.env.MONGO_URL);
//define the mongoose Schema
let mySchema = new mongoose.Schema({
///my mongoose schem
});
mySchema('toJSON', { virtuals: true }); //will include both id and _id
conn.model('mySchema', mySchema );
}
//Get the mongoose Model from the Schema
let mod = conn.model('mySchema');
switch(event.field) {
case "getOne": {
return mod.findById(event.context.arguments.id);
} break;
case "getAll": {
return mod.find()
} break;
default: {
throw new Error ("Lambda handler error: Unknown field, unable to resolve " + event.field);
} break;
}
}
C'est beaucoup mieux que mon autre "mauvaise" réponse parce que vous n'interrogez pas toujours une base de données pour obtenir des informations que vous avez déjà sur le côté client. Environ 3 fois plus vite dans mon expérience.
Pour rendre le nom d'utilisateur, l'e-mail, les sous-utilisateurs, etc. de l'utilisateur accessibles via l'API AppSync, il existe une réponse à cela: https://stackoverflow.com/a/42405528/1207523
En résumé, vous souhaitez envoyer un jeton d’ID de pools d’utilisateurs à votre API (par exemple, AppSync ou API Gateway). Votre demande d'API est authentifiée IAM. Ensuite, vous validez le jeton ID dans une fonction Lambda et vous disposez maintenant des données de votre utilisateur IAM et des pools d'utilisateurs validées.
Vous voulez utiliser le identity.cognitoIdentityId
du IAM comme clé primaire pour votre table d'utilisateurs. Ajoutez les données incluses dans le jeton d'identification (nom d'utilisateur, email, etc.) en tant qu'attributs.
De cette façon, vous pouvez rendre les revendications de l'utilisateur disponibles via votre API. Maintenant, par exemple, vous pouvez définir $ctx.identity.cognitoIdentityId
en tant que propriétaire d’un élément. Alors peut-être que d'autres utilisateurs pourront voir le nom du propriétaire via les résolveurs GraphQL.
Si vous avez besoin d'accéder aux revendications de l'utilisateur dans votre résolveur, je crains que cela ne semble pas être possible pour le moment. J'ai posé une question à ce sujet car cela serait très utile pour l'autorisation: Autorisation de groupe dans AppSync utilisant l'authentification IAM
Dans ce cas, au lieu d'utiliser un résolveur, vous pouvez utiliser Lambda en tant que source de données et récupérer les revendications de l'utilisateur à partir de la table User mentionnée ci-dessus.
C'est un peu difficile pour le moment :)
Voici une mauvaise réponse qui fonctionne. Je remarque que cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7"
contient le sous-utilisateur de Cognito (le plus important après CognitoSignIn). Vous pouvez extraire cela avec une expression rationnelle et utiliser aws-sdk pour obtenir les informations de l'utilisateur à partir du pool d'utilisateurs Cognito.
///////RETRIEVE THE AUTHENTICATED USER'S INFORMATION//////////
if(event.context.identity.cognitoIdentityAuthType === 'authenticated'){
let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
//Extract the user's sub (ID) from one of the context indentity fields
//the REGEX in match looks for the strings btwn 'CognitoSignIn:' and '"', which represents the user sub
let userSub = event.context.identity.cognitoIdentityAuthProvider.match(/CognitoSignIn:(.*?)"/)[1];
let filter = 'sub = \"'+userSub+'\"' // string with format = 'sub = \"1a072f08-5c61-4c89-807e-417d22702eb7\"'
let usersData = await cognitoidentityserviceprovider.listUsers( {Filter: filter, UserPoolId: "us-west-2_KsyTKrQ2M",Limit: 1}).promise()
event.context.identity.user=usersData.Users[0];
}
C'est une mauvaise réponse, car vous envoyez une requête ping à la base de données du pool d'utilisateurs au lieu de simplement décoder un fichier JWT.
Si vous utilisez AWS Amplify, ce que j'ai fait pour contourner ce problème consiste à définir un en-tête personnalisé username
comme expliqué ici , comme suit:
Amplify.configure({
API: {
graphql_headers: async () => ({
// 'My-Custom-Header': 'my value'
username: 'myUsername'
})
}
});
alors dans mon résolveur j'aurais accès à l'en-tête avec:
$context.request.headers.username
Comme expliqué par la documentation d'AppSync ici dans la section En-têtes de demande d'accès