Appsync Resolver no email in claim - amazon-cognito

I have created a resolver that uses the email address ($context.identity.claims.email). I tested my query in the AWS Console "Queries" section and all worked fine as $context.identity.claims looked as expected;
{
sub: 'xxx-xxx-xxx-xxx-xxx',
aud: 'xxxxxxxxx',
email_verified: true,
sub: 'xxx-xxx-xxx-xxx-xxx',
token_use: 'id',
auth_time: 1563643503,
iss: 'https://cognito-idp.ap-southeast-1.amazonaws.com/ap-southeast-1_xxxxx',
'cognito:username': 'xxxx',
exp: 1563647103,
iat: 1563643503,
email: 'xxx#xxx.xxx'
}
All looks good so lets use it in my React App that uses the AWS Amplify code for authentication. Its not working now and that is because there is no "email" in the claim section! It looks like this;
{
sub: 'xxx-xxx-xxx-xxx-xxx',
event_id: 'xxx-xxx-xxx-xxx-xxx',
token_use: 'access',
scope: 'aws.cognito.signin.user.admin',
auth_time: 1563643209,
iss: 'https://cognito-idp.ap-southeast-1.amazonaws.com/ap-southeast-1_xxxx',
exp: 1563646809,
iat: 1563643209,
jti: 'xxx-xxx-xxx-xxx-xxx',
client_id: 'xxxx',
username: 'xxxx'
}
Can anyone help me out as to why the email shows in the AWS Console Query but not when I call it from my own client?

Amplify can be configured to include the current ID Token for each graphql request by passing in a function. Two configuration options are shown in the following:
import { Auth } from 'aws-amplify';
const getIdToken = async () => ({
Authorization: (await Auth.currentSession()).getIdToken().getJwtToken()
});
const aws_exports = {
aws_appsync_graphqlEndpoint: 'https://****.appsync-api.us-east-2.amazonaws.com/graphql',
aws_appsync_region: 'us-east-2',
aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS',
// OPTION 1
graphql_headers: getIdToken,
// OPTION 2
// API: {
// graphql_headers: getIdToken
// },
Auth: {
identityPoolId: 'us-east-2:********-****-****-****-************',
region: 'us-east-2',
userPoolId: 'us-east-2_*********',
userPoolWebClientId: '*************************',
type: 'AMAZON_COGNITO_USER_POOLS'
}
};
export default aws_exports;
Amplify.configure(awsconfig);
Note the different claims available to the resolver between Access & ID tokens.
Access tokens will provide claims such as client_id, jti, and scope, while ID token claims provide email, phone_number, etc., along with others like aud, cognito:roles and cognito:username.
Access Token
{
"claims": {
"auth_time": 1581438574,
"client_id": "*************************",
"cognito:groups": [
"Admin"
],
"event_id": "ec70594c-b02b-4015-ad0b-3c207a18a362",
"exp": 1581442175,
"iat": 1581438575,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_*********",
"jti": "351d2d5f-13c3-4de8-ba7c-b3c5a9e46ca6",
"scope": "aws.cognito.signin.user.admin",
"sub": "********-****-****-****-************",
"token_use": "access",
"username": "********-****-****-****-************"
},
...
}
ID Token
{
"claims": {
"address": {
"formatted": "1984 Newspeak Dr"
},
"aud": "....",
"auth_time": 1581438671,
"birthdate": "1984-04-04",
"cognito:groups": [
"Admin"
],
"cognito:roles": [
"arn:aws:iam::012345678901:role/us-east-2-ConsumerRole"
],
"cognito:username": "********-****-****-****-************",
"email": "winston.smith#oceania.gov",
"email_verified": true,
"event_id": "e3087488-bfc8-4d08-a44c-089c4ae7d8ec",
"exp": 1581442271,
"gender": "Male",
"iat": 1581438672,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_*********",
"name": "WINSTON SMITH",
"phone_number": "+15551111984",
"phone_number_verified": false,
"sub": "********-****-****-****-************",
"token_use": "id"
},
...
}
Tested with amplify-js#2.2.4
Source: https://github.com/aws-amplify/amplify-js/blob/aws-amplify%402.2.4/packages/api/src/API.ts#L86-L107

Guessing that inside your React App, you are retrieving the user attributes with something to the effect of
import { Auth } from 'aws-amplify';
async componentDidMount() {
const currentUser = await Auth.currentUserInfo();
const claims = currentUser.attributes;
// verification logic here, and here you cannot find claims['email']
}
One thing to check is whether the specific React App client can access the 'email' attribute. The client may have been disallowed to specific attributes.
Inside the AWS Cognito Console > User Pools > General Settings > App Clients you should see something like the screen shot below.
Find the specific app client (match the Id). Click on 'Set attribute read and write permissions' - underlined red. There you should be able to select the email attribute as Readable by this client.

I have had a request for a feature in Amplify and I got the following brilliant solution suggestion
TLDR : Update your Auth Provider to create a "pre-token generation" Lambad, and inside your lambda you add another 'fake' group to the claim, as the groups are part of the token passed to AppSync
More details on the solution in this repo
https://github.com/dantasfiles/AmplifyMultiTenant

Ok, so I think they is in the "token_use" element. My original code used this function;
import {API, graphqlOperation} from 'aws-amplify';
import * as queries from '../../graphql/queries';
async function makeCall() {
let resp = await API.graphql(graphqlOperation(queries.getMeta));
return resp.data.getMeta;
}
That code produces the observed above. If I use the following (very dirty but works) code I get the above expected result;
import {Auth, API, graphqlOperation} from 'aws-amplify';
import axios from 'axios';
import * as queries from '../../graphql/queries';
async function makeCall() {
const curSesh = await Auth.currentSession();
const token = curSesh.idToken.jwtToken;
const resp = await axios({
method: 'post',
url: API._options.aws_appsync_graphqlEndpoint,
data: graphqlOperation(queries.getMeta),
headers: {
authorization: token
}
});
return resp.data.data.getMeta;
}
I am not going mark this as solved quite yet as I am sure there is a far cleaner way to get this working. If anyone can shed light on it I would love to learn.

Related

Unable to disconnect from a WalletConnect dApp

I am setting up the ability to disconnect the session from my wallet app in React-Native. My code looks like this which matches the docs except that I am importing "getSdkError" from WalletConnectUtils:
import { Core } from "#walletconnect/core";
import { Web3Wallet } from "#walletconnect/web3wallet";
import "#walletconnect/react-native-compat";
import * as WalletConnectUtils from '#walletconnect/utils';
import { CONNECT_WALLET_PROJECT_ID } from '#env';
const core = new Core({
projectId: CONNECT_WALLET_PROJECT_ID
});
const metadata = {
name: '<company name>',
description: 'A Wallet Application',
url: "#",
icons: []
};
const web3wallet = await Web3Wallet.init({
core,
metadata: metadata
});
const disconnect = await web3wallet.disconnectSession({
topic: topic,
reason: WalletConnectUtils.getSdkError("USER_DISCONNECTED"),
});
To completely match the docs I have also tried this:
const disconnect = await web3wallet.disconnectSession({
topic,
reason: WalletConnectUtils.getSdkError("USER_DISCONNECTED"),
});
When I run this though I get the error:
Object {
"context": "core",
}, Object {
"context": "core/pairing",
}, Object {
"code": 10001,
"message": "Unsupported wc_ method. wc_pairingDelete",
}
I checked to see what WalletConnectUtils.getSdkError("USER_DISCONNECTED") returns and got this:
Object {
"code": 6000,
"message": "User disconnected.",
}
I've been using https://react-app.walletconnect.com/ to test and it doesn't seem to disconnect on that site when I run this so I want to make sure I am disconnecting successfully.

Issue with state mismatch on registration using react native app auth

Im wondering if anyone has some experience in this issue.
I am getting a state mismatch error when trying to register using react native app auth.
React native version: "0.67.3",
React native app auth version: "^6.4.3",
[Error: State mismatch, expecting Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXs but got Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXsregistration in authorization response <OIDAuthorizationResponse: 0x6000017b29e0, authorizationCode: 4d890080dde715cedddf076e5ffb4fc8aaeeb22d4ebca281d4c7d74df377607c, state: "Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXsregistration", accessToken: "(null)", accessTokenExpirationDate: (null), tokenType: (null), idToken: "(null)", scope: "(null)", additionalParameters: {
}, request: <OIDAuthorizationRequest: 0x600001f7c000, request: https://api.staging.com/oauth/authorize?nonce=iJxSOkt6tGToBUndfg3n0V4B_ZZNBIm8TwbTg18EGOo&response_type=code&scope=trusted%20public%20refresh_token&code_challenge=iNlpVkj7UDpXyu5wBlMuln41huSZcGsdWEQ9fYLtcuU&code_challenge_method=S256&redirect_uri=someredirectt&client_id=9dc36c26d21198f5c97f12b34be3cce7a37e5abdc323fcc0b205a898d22994f7&state=Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXs>>]
the code that generates the request is so:
import { authorize } from 'react-native-app-auth';
const config = {
issuer: TEMP_API,
clientId: OAUTH_PUBLIC_CLIENT_ID,
redirectUrl: OAUTH_CALLBACK_URL,
clientSecret: OAUTH_CLIENT_SECRET,
scopes: ['trusted', 'public', 'refresh_token'],
};
const configForSignup = {
...config,
additionalParameters: {
response_mode: 'query'
},
};
export const authorizeOauthUser = (
{
isSignup,
},
) => async () => {
try {
const oAuthConfig = isSignup ? configForSignup : config;
const result = await authorize(oAuthConfig);
console.log({result});
} catch (error) {
console.log({error})
}
};
I have looked at the following ticket and implemented the suggested response_mode: "query" but to no avail.
Im not quite sure of the 'registration' at the end of the expected token [Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXsregistration] is appended to the token itself and thats why its mismatching or if its just spaced strangely.
A little bit old but the response mentions explicitly state: "Z2-6m8_T7FcIlbG9wep3Xb2wvgsylbd9M54iiX97rXsregistration"
I guess you are trying to connect to a custom authentication system, you should log that as a bug. The state parameter in the response should exactly match the one from the request.
The state is not the token, it is just a random ID to make sure that the response is received for the right request.

cypress-keycloak-commands using on keycloak login

I try to use cypress-keycloak-commands in my tests but always get this error:
I did everything accorig to this docu: https://www.npmjs.com/package/cypress-keycloak-commands
I don't understand how the code should know where to fill in the username an password. This is my code for the login:
it('Login', () => {
cy.visit(Cypress.env('GBS_URL'))
cy.kcLogout();
cy.kcLogin("user");
cy.visit("/"); })
What is the probleme here? I changed the user.json to my setings, added the env: { ... } to the json abd installed the package. Also added:import "cypress-keycloak-commands"; in the commands.js file.
The error comes from the keycloak library, and it's expecting to find a <form> element, but not finding it.
This is the piece of code where the error occurs.
const authBaseUrl = Cypress.env("auth_base_url");
const realm = Cypress.env("auth_realm");
const client_id = Cypress.env("auth_client_id");
cy.request({
url: `${authBaseUrl}/realms/${realm}/protocol/openid-connect/auth`,
followRedirect: false,
qs: {
scope: "openid",
response_type: "code",
approval_prompt: "auto",
redirect_uri: Cypress.config("baseUrl"),
client_id
}
})
.then(response => {
const html = document.createElement("html");
html.innerHTML = response.body;
const form = html.getElementsByTagName("form")[0];
const url = form.action;
The form should be part of the response.body, but since it's not there the request must be failing.
Check what you have in Cypress.env("auth_base_url"), Cypress.env("auth_realm") and Cypress.env("auth_client_id")
If you added them to cypress.json they would be similar to this
Ref Setup Keycloak configuration
Setup the Keycloak configuration in cypress.json configuration file:
{
"env": {
"auth_base_url": "https://auth.server/auth",
"auth_realm": "my_realm",
"auth_client_id": "my_client_id"
}
}

vuejs - using JWT as param in vue-router

I'm trying to build a vuejs app using quasar framework.
When a user enters his email account and clicks the reset password link that include a JWT as param (http://localhost:8080/reset-password/eyJhbGciOiJIUz...), he gets this error:
Cannot GET /reset-password/eyJhbGciOiJIUz....
This is the relevant code in routes.js:
{
path: '/reset-password/:token',
component: () => import('pages/profile/ResetPassword.vue')
}
When I remove the 2 dots in the JWT param, the VUE page is being loaded.
For example, this link doesn't work:
http://localhost:8080/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMTM3MCwiZXhwIjoxNjEzMjMxNDMwfQ.IEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
But this one does:
http://localhost:8080/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMTM3MCwiZXhwIjoxNjEzMjMxNDMwfQIEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
How can I solve it without changing the JWT token by removing the 2 dots?
the JWT token is composed of 3 parts (header, payload, verify signature) separated by periods
HEADER: ALGORITHM & TOKEN TYPE
in base 64
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
decode base 64
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD: DATA
in base 64
eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMJNTM3MCwiZXhzwjMxNjE
decode base 64
{
"user_id": 1,
"key": "c29cd3f660d3e1b864a3bf63d841e762",
"iat": 1613231370,
"exp": 1613231430
}
VERIFY SIGNATURE
IEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
what matters is the payload, this is sent through the url, and use it.
now if you want to recreate the complete jwt, the header will always be the same, the signature can be generated like this
HMACSHA256 (
base64UrlEncode (header) + "." +
base64UrlEncode (payload),
your-256-bit-secret
)
usually password recovery services only use payload
https://jwt.io
I was fighting with the same problem and solved it by adding the token as a query parameter. That means someone would call my url as
domain.com/my-path?token=a.b.c
The example is written for vue3
const myRoute = {
path: "my-path",
name: "My Page",
component: import("./App.vue"),
beforeEnter( to, from, next ) {
const { token } = to.query;
if (typeof token === "string") {
const jwtRegEx = /(^[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$)/;
const match = token.match(jwtRegEx);
if (match === null) {
next(false);
} else {
next();
}
} else {
next(false);
}
},
};
The beforeEach function is there to check that the token is indeed a jwt.

FeathersJS + Auth0: Authenticated but user not populated

I installed the Auth0 lock and can login on my client side, with an idToken in my localStorage.
I send this idToken to my API server - a FeathersJS server, which is basically an extension to an Express server. I get authenticated correctly using JWT, but the user is empty in my req object (it's called student here):
{ authenticated: true,
query: {},
provider: 'rest',
headers: { (truncated...) },
student: {},
payload:
{ iss: 'https://mydomain.eu.auth0.com/',
sub: 'myusername',
aud: 'myaudience',
exp: 1494125072,
iat: 1494089072 } }
The 5 last lines are the payload contained inside Auth0's idToken.
The fact that my student user object is empty is kind of normal when I think about it, because the app doesn't know how to link a Auth0 user to one of my database users. It's done by the username property. But how do I tell my Feathers app that? Is there a populateUser or something similar?
I remembered such a function in the old Feathers, but the new one uses the common hook's populate function: see here:
populateUser -> use new populate hook in feathers-hooks-common
So I tried this new populate hook, but unfortunately it's only an After hook, which doesn't make sense since I want to populate the user before making the request.
And now I'm stuck. I mean, I could write my own before hook which populates the user, but I bet there's some nicer way to achieve what I want.
Below is some relevant code:
authentication.js
const authentication = require('feathers-authentication');
const jwt = require('feathers-authentication-jwt');
const oauth2 = require('feathers-authentication-oauth2');
const Auth0Strategy = require('passport-auth0').Strategy;
module.exports = function () {
const app = this;
const config = app.get('authentication');
// Set up authentication with the secret
app.configure(authentication(config));
app.configure(jwt());
app.configure(oauth2({
name: 'auth0',
Strategy: Auth0Strategy,
}));
// The `authentication` service is used to create a JWT.
// The before `create` hook registers strategies that can be used
// to create a new valid JWT (e.g. local or oauth2)
app.service('authentication').hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies)
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
});
};
config file
{
...
"authentication": {
"entity": "student",
"service": "students",
"secret": "SAME_SECRET_AS_BELOW",
"strategies": [
"jwt"
],
"path": "/authentication",
"jwt": {
"header": {
"type": "access"
},
"audience": "SAME_AUDIENCE_AS_BELOW",
"subject": "anonymous",
"issuer": "https://mydomain.eu.auth0.com/",
"algorithm": "HS256",
"expiresIn": "1d"
},
"auth0": {
"clientID": "SAME_AUDIENCE_AS_ABOVE",
"clientSecret": "SAME_SECRET_AS_ABOVE",
"domain": "https://mydomain.eu.auth0.com/"
}
}
}