web-dev-qa-db-fra.com

AppSync: Obtention des informations sur l'utilisateur dans $ contexte lors de l'utilisation de AWS_IAM auth

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.

6
honkskillet

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.

2
honkskillet

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 :)

4
Mikael Lindlöf

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.

3
honkskillet

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

0
alejo4373