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

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

Related

Cookie is sent, but not stored when deployed

I'm having issues with cookies in a MERN app hosted to Vercel (Front-end) and Heroku (Back-end).
Everything is working fine in localhost, but when deployed i'm having issues storing cookies. Set-Cookie is sent with the sign in request, and the request itself looks fine. I get no errors.
After being signed in, authenticated routes return what I expect for each user, so the cookie seems to be accessible to the back-end even tho the cookie is not stored on the front-end under storage, which I assume is a security issue. This is not the case in Safari, in Safari the cookies are gone after login, so the user is logged out again.
The only route that does not work is signing out. On sign out I clear the token cookie, but it is trying to delete something that technically isn't there, still no errors. UPDATE: Clearing cookie works! I wasn't sending any data back with the sign out, so the front-end thought it was unresponsive, adding a .send({ message: 'Sign Out Successful' }) solved that issue. However, still no cookie visible in the front-end storage.
For CORS settings I have the origin set to the front-end url, and credentials set to true. While on the front-end I have withCredentials: true set on every request.
When deployed, the cookie is using sameSite: none, and secure: true.
Below are the sign in and sign out routes, but you can find the full back-end code here, and the front-end code here on GitHub.
Sign in route on back-end
import type Route from '../../types/Route'
import jwt from 'jsonwebtoken'
import env from '../../env/env'
import bcrypt from 'bcryptjs'
import User from '../../models/User'
import validateSignIn from '../../utils/validation/signIn'
const route: Route = {
method: 'post',
execute: async (req, res) => {
const { username, password } = req.body.user
try {
const { errors, valid } = validateSignIn(username, password)
if (!valid) return res.status(401).send({ errors })
const user = await User.findOne({ username })
if (!user) return res.status(404).send({ errors: { username: 'User not found' }})
const correctPassword = await bcrypt.compare(password, user.password)
if (!correctPassword) return res.status(401).send({ errors: { password: 'Wrong password' } })
const token = jwt.sign({ userId: user._id }, env.SECRET, { expiresIn: "1hr" })
return res.status(200).cookie('token', token, {
expires: new Date(Date.now() + 604800000),
secure: env.ENVIRONMENT === 'LIVE',
sameSite: env.ENVIRONMENT === 'LIVE' ? 'none' : 'lax',
httpOnly: true
}).send(user)
} catch (error) {
console.log('#sign/in', error)
return res.sendStatus(500)
}
}
}
export default route
Sign out route on back-end:
import type Route from '../../types/Route'
import authorization from '../../middlewares/http'
import env from '../../env/env'
// TODO: Fix Sign Out
const route: Route = {
method: 'get',
authorization,
execute: async (req, res) => {
try {
return res.clearCookie('token').sendStatus(200)
} catch (error) {
console.log('#sign/out', error)
return res.status(500)
}
}
}
export default route
The .clearCookie() doc says
Web browsers and other compliant clients will only clear the cookie if the given options is identical to those given to res.cookie(), excluding expires and maxAge.
So, try making your clear operation match your set operation, something like this.
res
.clearCookie('token', token, {
secure: env.ENVIRONMENT === 'LIVE',
sameSite: env.ENVIRONMENT === 'LIVE' ? 'none' : 'lax',
httpOnly: true })
.sendStatus(200)

Lambda functions do not trigger after cognito events

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

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.