Lambda functions do not trigger after cognito events - vue.js

I'm trying to implement an authentication workflow using AWS Cognito in order to sync my users table (Hasura graphql backend) with Cognito users, but the post confirmation Lambda does not trigger. The code of the Lambda function is as follow:
const axios = require('axios');
exports.handler = async (event,context,callback) => {
console.log(event);
const id=event.request.userAttributes.sub;
const name=event.request.userAttributes.username;
const email=event.request.userAttributes.email;
const hasuraAdminSecret="####"
const graphAPI="####"
const body = {
query: `
mutation insertUsers($userId: String!, $userName: String!, $userEmail: String!) {
insert_users(objects: {cognito_id: $userId, email: $userEmail, username: $userName}) {
affected_rows
}
}
`,
variables: {
userId:id,
userEmail:name,
userName:email
}
}
var response = {};
await axios.post(graphAPI, body, {
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': hasuraAdminSecret}
})
.catch(err => {
console.error(err.data);
response=err.data;
})
.then(res => {
console.log(res.data);
response = res.data;
})
callback(null,event);
}
The code for the signup and the confirmation pages are as follow:
import { Auth } from 'aws-amplify';
export default {
name:'Signin',
data(){
return {
username: undefined,
email: undefined,
password: undefined,
code: undefined,
user: undefined,
}
},
methods: {
confirm() {
// After retrieveing the confirmation code from the user
Auth.confirmSignUp(this.username, this.code, {
// Optional. Force user confirmation irrespective of existing alias. By default set to True.
forceAliasCreation: false
}).then(this.$router.push("/"))
.catch(err => console.log(err));
},
signup(){
Auth.signUp({
username:this.username,
password: this.password,
attributes: {
email: this.email,
name:this.username
},
validationData: [], // optional
})
.then(data => this.user = data.user)
.catch(err => console.log(err));
}
}
}
When signing up the user is created and confirmed in the AWS console, but the lambda function is not triggered (no logs in Cloudwatch and no errors from Cognito). Where should I look ?

Once the new user signup through aws-cognito you can call lambda functions using trigger
Step 1: Open your aws-cognito User Pools under general setting click on trigger
Step 2: You can customise the workflow with triggers. You can call your lambda function
Pre sign-up
Pre authentication
Custom message
Post authentication
Post confirmation
Define Auth Challenge
Create Auth Challenge
Verify Auth Challenge
User Migration
Pre Token Generation
Step 3: Select your workflow trigger Post confirmation and you can see the list of lambda functions. You have to select the lambda function.
Adding Cloudwatch:
Step 1: You have add role under Configuration under Permissions tab
Step 2: Edit the role and attach policy for CloudWatch and CloudWatch Logs

Related

Cognito '/oauth2/token' end point not returning 'id_token' for Authorization Code Grant with PKCE

Cognito '/oauth2/token' end point not returning 'id_token' for Authorization Code Grant with PKCE even though the documentation says it will be returned (Link). It should return the id_token as well. Is this normal or I need to configure more? I have added the content of the git issue opened by me below if this is helpful(Issue 7393)
To Reproduce
Steps to reproduce the behavior:
Configure the user pool with hosted UI support
Go to hosted UI and complete the login
User will be redirected to the "redirectSignIn" URL
Hub.listen('auth') event fired with error message "Sign in failure Error: Username and Pool information are required."
Check the session for ID token
Check the code challenge request to get the tokens(/oauth2/token request)
Both do not have the ID token. /oauth2/token only returns access_token, expires_in, refresh_token and token_type
Expected behavior
It should also return id_token
Code Snippet
import React, {useEffect, useState} from 'react';
import { Amplify, Auth, Hub } from 'aws-amplify';
import {AmplifyConfig} from '../../config/amplifyConfig';
Amplify.configure({
Auth: AmplifyConfig.auth
});
const AuthorizePage = (props: any) => {
const [user, setUser] = useState(null);
useEffect(() => {
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
case 'cognitoHostedUI':
getUser().then(userData => setUser(userData));
break;
case 'signOut':
setUser(null);
break;
case 'signIn_failure':
case 'cognitoHostedUI_failure':
console.log('Sign in failure', data);
break;
}
});
getUser().then(userData => setUser(userData));
}, []);
function getUser() {
return Auth.currentAuthenticatedUser()
.then(userData => userData)
.catch(() => console.log('Not signed in'));
}
return (
<div className="menu-card-filter--items" data-id="aperitif">
<span>
Authorizing
</span>
</div>
)
}
export default AuthorizePage;
Screenshots
Request
https://user-images.githubusercontent.com/12485276/101932415-bccab580-3c00-11eb-8cde-222d72f0d956.png
Response
https://user-images.githubusercontent.com/12485276/101932467-d3710c80-3c00-11eb-9d3b-778faee43fa4.png
What is Configured?
Auth: {
mandatorySignIn: true,
region: "******-*",
userPoolId: "**-******-*_*******",
userPoolWebClientId: "**********************",
oauth: {
domain: "**********************.amazoncognito.com",
scope: [
"phone",
"email",
"profile",
],
redirectSignIn: "http://localhost:3000/authorize",
redirectSignOut: "http://localhost:3000/logout",
responseType: "code"
}
}
Add the 'openid' scope to the scope list in your auth configuration.
The openid scope returns all user attributes in the ID token that are readable by the client. The ID token is not returned if the openid scope is not requested by the client.
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.htmlhere

How to test single page application with Cypress and Auth0

I am having a single page application hidden behind Auth0 lock, using #auth0/auth0-spa-js. I would like to test it using Cypress, so I have decided to follow the official Auth0 blog post, as well as Johnny Reilly blog post.
I am able to successfully retrieve valid JWT token from auth0 using suggested request. I have no idea what to do with it :(
The trouble I am facing is that both of the above approaches are relying on the app to store the JWT token locally (either in cookie or localstorage). The #auth0/auth0-spa-js is, however, using a different approach, and I assume all the relevant cookies/localstorage is stored on auth0 domains.
Do you have any idea, if there is a way to get around it?
There is a similar issue reported here raised in July 2018, not really providing any solution
I found a resolved issue on #auth0/auth0-spa-js github. The approach suggested by cwmrowe seems to be working
The solution is to mock the response of oauth/token endpoint with token generated on e2e test side.
The approach seems to be working for us
I am copying over the sample code cwmrowe has provided
Cypress.Commands.add(
'login',
(username, password, appState = { target: '/' }) => {
cy.log(`Logging in as ${username}`);
const options = {
method: 'POST',
url: Cypress.env('Auth0TokenUrl'),
body: {
grant_type: 'password',
username,
password,
audience: Cypress.env('Auth0Audience'),
scope: 'openid profile email',
client_id: Cypress.env('Auth0ClientId'),
client_secret: Cypress.env('Auth0ClientSecret')
}
};
cy.request(options).then(({ body }) => {
const { access_token, expires_in, id_token } = body;
cy.server();
// intercept Auth0 request for token and return what we have
cy.route({
url: 'oauth/token',
method: 'POST',
response: {
access_token,
expires_in,
id_token,
token_type: 'Bearer'
}
});
// Auth0 SPA SDK will check for value in cookie to get appState
// and validate nonce (which has been removed for simplicity)
const stateId = 'test';
const encodedAppState = encodeURI(JSON.stringify(appState));
cy.setCookie(
`a0.spajs.txs.${stateId}`,
`{%22appState%22:${encodedAppState}%2C%22scope%22:%22openid%20profile%20email%22%2C%22audience%22:%22default%22}`
);
const callbackUrl = `/auth/callback?code=test-code&state=${stateId}`;
return cy.visit(callbackUrl);
});
}
);
declare namespace Cypress {
interface Chainable<Subject> {
login(
username: string,
password: string,
appState?: any
): Chainable<Subject>;
}
}
Whilst it's not recommended to use the UI to login I do this myself once prior to all tests and then use the silent auth for the tests:- cy.visit("/") silent auths and allows access to the app.
integration/app.js
describe("App", () => {
before(() => {
Cypress.config("baseUrl", "http://localhost:3000");
cy.login();
});
/** Uses silent auth for successive tests */
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
/** tests */
support/commands.js
/**
* Auth0 login
* https://github.com/cypress-io/cypress/issues/461#issuecomment-392070888
*
* Allows silent auth login between tests
*/
let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach(key => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
Cypress.Commands.add("clearLocalStorage", () => {
LOCAL_STORAGE_MEMORY = {};
});
For those who has issue with Google Sign in for Cypress look at the plugin: https://github.com/lirantal/cypress-social-logins/
it('Login through Google', () => {
const username = Cypress.env('googleSocialLoginUsername')
const password = Cypress.env('googleSocialLoginPassword')
const loginUrl = Cypress.env('loginUrl')
const cookieName = Cypress.env('cookieName')
const socialLoginOptions = {
username,
password,
loginUrl,
headless: false,
isPopup: true,
logs: false,
loginSelector: 'a[href="/auth/auth0/google-oauth2"]',
postLoginSelector: '.account-panel'
}
return cy.task('GoogleSocialLogin', socialLoginOptions).then(({cookies}) => {
cy.clearCookies()
const cookie = cookies.filter(cookie => cookie.name === cookieName).pop()
if (cookie) {
cy.setCookie(cookie.name, cookie.value, {
domain: cookie.domain,
expiry: cookie.expires,
httpOnly: cookie.httpOnly,
path: cookie.path,
secure: cookie.secure
})
Cypress.Cookies.defaults({
whitelist: cookieName
})
}
})
});

How do I configure Amplify to to use multiple AppSync endpoints?

I need to support authenticated and unauthenticated AppSync requests in a React Native app. Since AppSync only allows one authorization type per API, I am setting up two APIs: one for authenticated users (Cognito User Pools), and one for guests (API Key).
I think to make this work I need to have two distinct AWSAppSyncClient configs in the same app.
// authenticated user
const appSyncAuthenticatedClient = new AWSAppSyncClient({
url: Config.APPSYNC_AUTHENTICATED_ENDPOINT,
region: Config.APPSYNC_REGION,
auth: {
type: 'AMAZON_COGNITO_USER_POOLS',
jwtToken: async () =>
(await Auth.currentSession()).getAccessToken().getJwtToken()
}
});
// guest
const appSyncUnauthenticatedClient = new AWSAppSyncClient({
url: Config.APPSYNC_UNAUTHENTICATED_ENDPOINT,
region: Config.APPSYNC_REGION,
auth: {
type: 'API_KEY',
apiKey: Config.APPSYNC_API_ID
}
});
and then determine which to use based on whether or not they are logged in
Auth.currentAuthenticatedUser()
.then(user => this.appSyncRunningClient = appSyncAuthenticatedClient)
.catch(err => this.appSyncRunningClient = appSyncUnauthenticatedClient);
const App = props => {
return (
<ApolloProvider client={this.appSyncRunningClient}>
<Rehydrated>
<RootStack/>
</Root>
</Rehydrated>
</ApolloProvider>
);
};
export default App;
This fails because currentAuthenticatedUser returns a promise, and I'm stuck at how to resolve a promise at this top level instantiation of the app. I'll also need to swap out this config during auth events.
In what way can I dynamically select and change the ApolloProvider config at startup and authentication events?
This is currently not possible. Until top-level await is officially supported you should create two Apollo clients one for the API and one for the Cognito.
for example: in your App.js
export default function App(props) {
const [client, setClient] = useState(null);
useEffect(() => {
checkAuth()
}, []);
function checkAuth() {
Auth.currentSession().then(session => {
const token = session.getIdToken();
const jwtToken = token.getJwtToken();
if (typeof jwtToken == "string") {
const authClientConfig = {
url: awsmobile.aws_appsync_graphqlEndpoint,
region: awsmobile.aws_appsync_region,
disableOffline: true,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: jwtToken
}
}
const link = ApolloLink.from([createAuthLink(authClientConfig), createSubscriptionHandshakeLink(authClientConfig)]);
const authClient = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }) });
setClient(authClient);
} else {
throw "error";
}
}).catch(e => {
console.log(e);
const config = {
url: awsmobile.aws_appsync_graphqlEndpoint,
region: awsmobile.aws_appsync_region,
disableOffline: true,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: awsmobile.aws_appsync_apiKey
}
}
const link = ApolloLink.from([createAuthLink(config), createSubscriptionHandshakeLink(config)]);
const authClient = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }) });
setClient(authClient);
})
}
if (!client) {
return "Loading..."
}
return (
<ApolloProvider client={client}>
...
</ApolloProvider>
);
}`
Things may have moved on as AppSync now supports multiple authentication types per API; however providing an answer as to how to auth/unauth on same endpoint for prosperity. Doesn't answer the how-to multiple endpoints question which is what led me here, but that's no longer required in OPs scenario.
Note: This answer applies to typescript - I'm not overly familiar with react but I think it will work exactly the same...
Unauthenticated access uses AWS_IAM / i.e. CognitoIdentityPool
(configured to allow unauthenticated access)
Authenticated Access users AMAZON_COGNITO_USER_POOLS authentication.
To switch between unauthenticated and authenticated API.graphql() calls. You need to test the current authentication status and use that to override the authMode as in the arguments to the API.graphql() call.
As a prerequisite:
The types in graphql must be setup to allow access via both #aws_iam and #aws_cognito_user_pools (see sample below)
The AppSync API must be configured to allow both authentication types (The code below assumes the API is configured for AWS_IAM by default, but allowed CognitoUserPools as an additional authentication type). This can be configured in console, or via cloudFormation.
Sample code for API call
let authMode;
try {
authMode = (await Auth.currentUserPoolUser()) ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : undefined;
} catch (err) { }
const result = await API.graphql({
...graphqlOperation(statement, gqlAPIServiceArguments),
authMode
});
Example grqphql type
type Profile #aws_iam #aws_cognito_user_pools {
username: ID!
stuff: String!
}
My Amplify Configuration
{
aws_project_region: 'VALUE_HERE',
aws_appsync_graphqlEndpoint: 'https://VALUE_HERE/graphql',
aws_appsync_region: 'VALUE_HERE',
aws_appsync_authenticationType: 'AWS_IAM',
aws_appsync_apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXX', // This field seems to be required, but the value is ignored.
Auth: {
identityPoolId: 'VALUE_HERE',
region: 'VALUE_HERE',
userPoolId: 'VALUE_HERE',
userPoolWebClientId: 'VALUE_HERE',
oauth: {
domain: 'VALUE_HERE',
redirectSignIn: 'VALUE_HERE',
redirectSignOut: 'VALUE_HERE',
scope: ['email', 'openid', 'profile', 'aws.cognito.signin.user.admin'],
responseType: 'code'
}
}
};

JWT authentication with AXIOS

Using Vue webpack template, trying to make JWT authentication. What I've done so far:
"src/auth/index.js":
// Send a request to the login URL and save the returned JWT
login (creds, redirect) {
axios.post(LOGIN_URL, creds, (data) => {
localStorage.setItem('access_token', data.access_token)
this.user.authenticated = true
// Redirect to a specified route
if (redirect) {
router.push(redirect)
}
}).error((err) => {
context.error = err
})
},
I'm calling this function from LoginPage.vue:
methods: {
login () {
var credentials = {
username: this.credentials.username,
password: this.credentials.password
}
// We need to pass the component's this context
// to properly make use of http in the auth service
auth.login(this, credentials, 'requests')
}
}
When I'm submitting the form, data is submitted, but I get the following error in a console:
TypeError: __WEBPACK_IMPORTED_MODULE_1_axios___default.a.post(...).error is not a function
Also JWT token is not saving in my local storage, what am I doing wrong?
Rewrote login function:
login (context, creds, redirect) {
axios.post(LOGIN_URL, creds)
.then((response) => {
localStorage.setItem('access_token', response.data.access_token)
this.user.authenticated = true
if (redirect) {
router.push(redirect)
}
}).catch((err) => {
context.error = err.response.data
})
},
Everything is working now.

Request vuejs axios after auto login completed

How to execute all other request after auto login wass passed. Code example.
axios.get('personal/' + this.$store.state.username + '/config/', {headers: { Authorization: 'Token ' + this.$store.state.idToken }})
Sometimes request that receive user data (username and id) have no time to passed and commit to state, and i receive an error that username is null at state.
I have solve that problem by add in login func to save username and id in localstorage, and after in try auto login i have next code:
tryAutoLogin ({ commit, dispatch }) {
const token = localStorage.getItem('token')
if (!token) {
return
} else {
commit('getToken', {
key: token
})
const userId = localStorage.getItem('userId')
const username = localStorage.getItem('username')
if (!userId || !username) {
dispatch('getUser')
} else {
commit('getUserData', {
id: userId,
username: username.username
})
}
}
Is this way is ok? or there is any way to stop anny request to api, till the dispatch('getUser') will be passed succesfully.
example of getUser code:
getUser ({ commit, state }) {
if (!state.idToken) {
return
}
axios.get('rest-auth/user/', {headers: { Authorization: 'Token ' + state.idToken }})
.then(res => {
localStorage.setItem('username', res.data.username)
localStorage.setItem('userId', res.data.pk)
commit('getUserData', {
id: res.data.pk,
username: res.data.username
})
})
},
Plz, don't be strict i am new in FE vue js:)
First of all, make names of getters, actions, mutations and state more clean and obvious (getUser for getters and setUser for action, for example).
I recommend to create a separated auth module (place all auth logic in this module) and use it in Vuex actions or somewhere in application.
Such module should interact with the Store via Vuex getters, setters and actions (getting and setting current user auth status, for example). It makes authentification more incapsulated and clear.
In this way you'll be able to call this module's methods from any of application component and wait for the result.
In code bellow (http_auth.js) this.$auth is separated authentification module, that can set user state in Vuex and get current status. Also it use localStorage to check for saved token (user data) and tries to authorize with saved token (tryAutoLogin in your case). On fail, it makes redirect to login page.
...
methods: {
async loadInitialData () {
if (await this.$auth.init()) {
axios.get('initial-data-url').then(res => ...)
}
}
},
created () {
this.loadInitialData()
}
...
Auth methods are Promise-based, so you just can wait for resolving or rejecting before.
If you just want to use Vuex-only solution, you should use actions to call API-requests and wrap them in Promises. Also you can dispatch some action inside of other (for example, try login with saved token inside of basic login action).
Sample code (Vuex action):
LOAD_SOME_DATA ({ commit, state, getters }, id) {
return new Promise((resolve, reject) => {
if (!id) {
router.push('/')
return reject('Invalid ID passed.')
}
return axios.get(getters.GET_SOME_URL + id).then(response => {
commit('PUSH_SOME_DATA', response.data)
return store.dispatch('PROCESS_SOME_DATA').then(result => {
return resolve(response)
}, error => {
console.error('Error loading some data: ', error)
return reject(error)
})
}, error => {
router.push('/')
return reject(error)
})
})
}
Above we wrap in promise basic api-call (axios.get(getters.GET_SOME_URL + id)), then process received data (PROCESS_SOME_DATA).
Then we can use it in router, for example (or any other part of app):
store.dispatch('LOAD_SOME_DATA', to.params.id).then(result => ...)