Context:
I am using the amazon-cognito-identity-js SDK for authentication, as I am not using amplify for this project, only need to use cognito services. So far locally, I can do pretty much every fine, tokens come back and using the new AmazonCognitoIdentity.CookieStorage() it seems to be to store cookies locally using ({ domain: 'localhost', secure: 'false' }).
Also using nextjs v10.0.6
Problem
I tried to deploy the app to netlify and after installing it gives me back the tokens but does not store them in cookies on my browser.
Here is the snippet of code that I am using to sign in a user, there is a use case where the user was created by the admin, and will be forced to change password, thus the redirect to /changePassword
Any guidance would be amazing! My suspicion is that I am not configuring the domain right... but have tried every combination such as, removing the https, only including the autoGenerated subdomain part, etc
export const userPoolData = (): ICognitoUserPoolData => ({
UserPoolId: process.env.USER_POOL_ID || '',
ClientId: process.env.CLIENT_ID || '',
Storage: new CookieStorage({
domain: 'https://<autoGeneratedURL>.netlify.app',
secure: true,
expires: 10,
path: '/',
}),
});
const authenticationData = {
Username: username,
Password: password,
};
const authenticationDetails = new AuthenticationDetails(authenticationData);
const poolData = userPoolData();
const userPool = new CognitoUserPool(poolData);
console.log({ poolData });
const userData = {
Username: username,
Pool: userPool,
Storage: new CookieStorage({
domain: 'https://<autoGeneratedURL>.netlify.app',
secure: true,
expires: 10,
path: '/',
}),
};
const cognitoUser: CognitoUser = new CognitoUser(userData);
const userTokens: Pick<ResponseMessage, 'tokens'> = {};
console.log(authenticationData);
const authResponse = new Promise(() => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
console.log(result);
router.push({ pathname: '/' });
},
onFailure: (error) => {
console.log(error);
if (error.code !== 'InvalidParameterException' && error.code !== 'NotAuthorizedException') {
router.push({ pathname: '/changePassword', query: { username, password } });
}
},
});
});
try {
await authResponse;
} catch (error) {
return {
success: false,
code: 500,
error,
};
}
In case anyone runs into this, it turned out my suspicion was right and the fault was in the domain pattern, for a netlify app it should be configured as domain: <autoGeneratedURL>.netlify.app
Related
Using SvelteKit 1.0.0-next.571
My application has a login route with:
+page.server.ts => redirects to / if locals.user is set
+page.svelte => show login page
signin/+server.ts => Login and get a jwt from graphql app running on the same machine.
+server.ts:
[..]
let gqlResponse = await response.json();
if ( gqlResponse.errors ) {
console.log("ERRORS FROM GRAPHQL MIDDLEWARE:", gqlResponse.errors);
return json( { error: gqlResponse.errors, isException: true } );
}
if (gqlResponse.data.login.user && !gqlResponse.data.login.error) {
opts.cookies.set('jwt', gqlResponse.data.login.token, {
path: '/',
maxAge: SESSION_MAX_AGE
});
opts.setHeaders( { 'Access-Control-Allow-Credentials': 'true' } )
opts.setHeaders({ 'Content-Type': 'application/json' })
}
return json( gqlResponse.data.login );
and the login handler in +page.svelte :
[..]
const fetchOptions = {
method: 'POST',
//mode: 'no-cors',
//redirect: 'follow' as RequestRedirect,
body: JSON.stringify(credentials),
credentials: 'include' as RequestCredentials
}
try {
const response = await fetch('/login/signin', fetchOptions);
const login = await response.json();
if (login.error) {
handleError(login);
return false;
}
} catch (e) {
return handleException(e);
}
goto('/', { replaceState: true, invalidateAll: true} );
This works fine in localhost, but connecting another device to the local network does not set any cookies making impossible to login:
Local: http://localhost:5173/
➜ Network: http://192.168.x.x:5173/
I also tried with different fetch options and cookie settings like:
opts.cookies.set('jwt', gqlResponse.data.login.token, {
path: '/',
httpOnly: true,
sameSite: 'strict',
// secure: true
maxAge: SESSION_MAX_AGE
});
but no luck, and now 'm stuck.
I am implementing linking of user accounts in cognito that have the same email. So if someone signs up e.g. with Google and the email is already in cognito, I will link this new account to existing with AdminLinkProviderForUser. I have basically been following this answer here: https://stackoverflow.com/a/59642140/13432045. Linking is working as expected but afterwards email_verified is switched to false (it was verified before). Is this an expected behavior? If yes, then my question is why? If no, then my question is what am I doing wrong? Here is my pre sign up lambda:
const {
CognitoIdentityProviderClient,
AdminLinkProviderForUserCommand,
ListUsersCommand,
AdminUpdateUserAttributesCommand,
} = require("#aws-sdk/client-cognito-identity-provider");
exports.handler = async (event, context, callback) => {
if (event.triggerSource === "PreSignUp_ExternalProvider") {
const client = new CognitoIdentityProviderClient({
region: event.region,
});
const listUsersCommand = new ListUsersCommand({
UserPoolId: event.userPoolId,
Filter: `email = "${event.request.userAttributes.email}"`,
});
try {
const data = await client.send(listUsersCommand);
if (data.Users && data.Users.length) {
const [providerName, providerUserId] = event.userName.split("_"); // event userName example: "Facebook_12324325436"
const provider = ["Google", "Facebook", "SignInWithApple"].find(
(p) => p.toUpperCase() === providerName.toUpperCase()
);
const linkProviderCommand = new AdminLinkProviderForUserCommand({
DestinationUser: {
ProviderAttributeValue: data.Users[0].Username,
ProviderName: "Cognito",
},
SourceUser: {
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: providerUserId,
ProviderName: provider,
},
UserPoolId: event.userPoolId,
});
await client.send(linkProviderCommand);
/* fix #1 - this did not help */
// const emailVerified = data.Users[0].Attributes.find(
// (a) => a.Name === "email_verified"
// );
// if (emailVerified && emailVerified.Value) {
// console.log("updating");
// const updateAttributesCommand = new AdminUpdateUserAttributesCommand({
// UserAttributes: [
// {
// Name: "email_verified",
// Value: "true",
// },
// ],
// UserPoolId: event.userPoolId,
// Username: data.Users[0].Username,
// });
// await client.send(updateAttributesCommand);
// }
/* fix #2 - have no impact on the outcome */
// event.response.autoConfirmUser = true;
// event.response.autoVerifyEmail = true;
}
} catch (error) {
console.error(error);
}
}
callback(null, event);
};
As you can see, I tried passing autoConfirmUser and autoVerifyEmail which had no impact. And I also tried to manually update email_verified after calling AdminLinkProviderForUser which also did not help. So I think email_verified is set to false only after the lambda is finished.
I've actually find a solution for this issue! It's tricky, but it works quite well.
The main problem i was having is the fact that pre/post auth lambdas do not trigger if the user is logging in with the connected provider account. Nor does the post confirmation lambda triggers after the signUp link. On top of that, if you manually try to alter the provider's user inside cognito, you still end up with the email_verified = false.
So whats the solution here?
Well, it's actually two in one.
1. Attribute Mapping
Some of the providers supported by Cognito already have an 'email verified' attribute we can directly map in the Attribute Mapping section. That is the case with Google. Simply map it to Cognito's attribute and you are done!
2. PreToken trigger
For the other providers, mainly Facebook (i didnt use any provider other than Facebook and Google, but it should all work the same), that don't natively have the email verified attribute, the only way i could find to properly enforce it to be verified was through the pre-token trigger. The logic goes: once the lambda is invoked, verify if the user getting authenticated have a provider linked to it and, at the same time, have its email_verified options to false. If you meet this condition, update the user with the adminUpdateUserAttributes method before returning to Cognito. That way, it doesnt matter if any subsequent provider login flips the attribute back to false, this lambda ensures it's flipped back on.
If you want a sample code for the solution, here's mine pre-token handler:
const AWS = require('aws-sdk');
class PreTokenHandler {
constructor({ cognitoService }) {
this.cognitoService = cognitoService;
}
async main(event, _context, callback) {
const {
userPoolId,
userName: Username,
request: { userAttributes: { identities, email_verified } }
} = event;
const emailVerified = ['true', true].some(value => value === email_verified);
if (!identities?.length || emailVerified) return callback(null, event);
await this.cognitoService.adminUpdateUserAttributes({
UserPoolId: userPoolId,
Username,
UserAttributes: [{ Name: 'email_verified', Value: 'true' }]
}).promise();
callback(null, event);
}
}
const cognitoService = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
const handler = new PreTokenHandler({ cognitoService });
module.exports = handler.main.bind(handler);
I've added the 'true' x true check to ensure i'm safe on possibly inconsistencies on the attribute value, although it's just being extra safe and probably not necessary.
My answer is the same as #fabio, here's how you do it in aws-sdk-js-v3:
const {
CognitoIdentityProviderClient,
AdminUpdateUserAttributesCommand,
} = require('#aws-sdk/client-cognito-identity-provider')
const client = new CognitoIdentityProviderClient({
region: process.env.REGION,
})
exports.handler = async(event, context, callback) => {
try {
const {
userPoolId,
userName,
request: {
userAttributes: { email_verified }
}
} = event
if (!email_verified) {
const param = {
UserPoolId: userPoolId,
Username: userName,
UserAttributes: [{
Name: 'email_verified',
Value: 'true',
}],
}
await client.send(new AdminUpdateUserAttributesCommand(param))
}
callback(null, event)
}
catch (err) {
console.error(err)
}
}
Hello i use Electron JS for a desktop app which is related to a cloud plateform from which in needto get a list of Patients.
As far as now i can get it but with a static AccessToken. I really struggled to get it dynamic, please help.
Here is my code :
This is my configuration file where i specify Cognito Parameters :
export default {
s3: {
REGION: 'YOUR_S3_UPLOADS_BUCKET_REGION',
BUCKET: 'YOUR_S3_UPLOADS_BUCKET_NAME',
},
apiGateway: {
REGION: 'YOUR_API_GATEWAY_REGION',
URL: 'YOUR_API_GATEWAY_URL',
},
cognito: {
REGION: 'eu-west-1',
USER_POOL_ID: 'eu-west-1_P0Jcr7nig',
APP_CLIENT_ID: '4m1utu56hjm835dshts9jg63ou',
IDENTITY_POOL_ID: 'YOUR_IDENTITY_POOL_ID',
authenticationFlowType: 'USER_PASSWORD_AUTH',
AUTHENTICATION_FLOW_TYPE: 'USER_PASSWORD_AUTH',
},
API: {
endpoints: [
{
name: 'PatientsList',
endpoint: 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage/patients',
//endpoint: 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage',
},
],
},
};
Auth.signIn({
username: process.env.username,
password: process.env.password,
}).then().catch(err => {
console.log(err)});
In another file this is my getaccesstoken function which i export to the main
function getAccessToken() {
const poolData = {
UserPoolId : COGNITO_USER_POOL_ID,
ClientId : COGNITO_CLIENT_ID,
};
const userPool = new CognitoUserPool(poolData);
var authenticationData = {
Username : process.env.username, // your username here
Password : process.env.password, // your password here,
authenticationFlowType: process.env.AUTHENTICATION_FLOW_TYPE,
Pool : userPool
};
var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
authenticationData);
var cognitoUser = new CognitoUser(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
console.log('access token + ' + result.getAccessToken().getJwtToken());
},
onFailure: function(err) {
console.log(err);
},
});
}
And finally here is how i get the data in main :
The declarations :
const { Auth } = require('./cognitoAuth');
const theAccessToken = require('./cognitoAuth');
The code :
//Get Data From Cloud ECS
const API_URL = 'https://uo992r7huf.execute-api.eu-west-1.amazonaws.com/Stage/patients';
const headers = {
"Content-Type": "application/json",
//Authorization: theAccessToken.getAccessToken()
Authorization: "eyJraWQiOiJBbE1DZnBCTHYyVUlNazhXSG4xaTk4RG1QNlFmcFpSSjFaSW1qcVVFZnVBPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI4OWYyZGMxZi1iMTI3LTQzM2QtODJhYS1iMjNkNWJhNzY5NGEiLCJjb2duaXRvOmdyb3VwcyI6WyJkb2N0b3IiXSwiZXZlbnRfaWQiOiI1OTM0ZmIwNC0yYTUzLTQ2NmQtYTU1Ni0zNTM3M2RhZmU1Y2UiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiYXV0aF90aW1lIjoxNTk1NDI2NjQ2LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtd2VzdC0xLmFtYXpvbmF3cy5jb21cL2V1LXdlc3QtMV9QMEpjcjduaWciLCJleHAiOjE1OTcxNTUxMDUsImlhdCI6MTU5NzE1MTUwNSwianRpIjoiNGRkN2U5ZGUtYmQ2YS00NTg4LWIzZDAtMTVjMWM1NWQxY2Y2IiwiY2xpZW50X2lkIjoiNG0xdXR1NTZoam04MzVkc2h0czlqZzYzb3UiLCJ1c2VybmFtZSI6Ijg5ZjJkYzFmLWIxMjctNDMzZC04MmFhLWIyM2Q1YmE3Njk0YSJ9.LYvzPRBxvKw2P3gHwV8NhYPg_EB3F7ZK2F5HpRWHtBHksr6D4N5Fw56ZVupkRCxVJSq0f93DdljI7BBcBnp9d_hpLzmJLTfBhA3t870omTxalTqpGXN_SvsZmuwcfCX-awn1Um6x_-fhq3zcfPkB9FBljbtwPN-kvCc-Iynei9anVxXI686nIbkfbYfnuRnHrbY0vg8FtyDIDBMv277FPoJ96NwPD4mJvNBxQHi_KfWxQ1DmLiAC6c_l2jP_wawAPBv788CjD_8OlKBbjAHinGEkaL1K9vjI5MhNPyTA5ym1IaWar7Jr8RkUDzQGvqEUPKoOUe9PswmOOxLBjehMgQ"
};
//console.log('Token Value:', theAccessToken.getAccessToken());
const getPatients = async(API_URL) => {
try {
//get data from cloud specifiying get method and headers which contain token
const response = await fetch(API_URL,{
method: 'GET', headers: headers}
);
var listPatients = await response.json();
listPatients.items.forEach(patient => {
//Checking what i got
console.log(patient);
});
} catch(err) {
console.log("Error" + err);
}
};
getPatients(API_URL);
Now when i make it dynamic by specifying theAccessToken.getAccessToken
I get this error, USER_SRP is not enabled even if specify it, when i asked team told me the cloud service doesn't want to enable it.
So how can i get this access token please?
For a desktop app it is recommended to do these 2 things, according to security guidance:
Use Authorization Code Flow (PKCE)
Login via the system browser, so that the app never sees the user's password
I have a couple of Electron code samples that use Cognito, which you can easily run - maybe start here:
First desktop app
I have the following set up in my nuxt.config.js file:
auth: {
redirect: {
login: '/accounts/login',
logout: '/',
callback: '/accounts/login',
home: '/'
},
strategies: {
local: {
endpoints: {
login: { url: 'http://localhost:8000/api/login2/', method: 'post' },
user: {url: 'http://localhost:8000/api/user/', method: 'get', propertyName: 'user' },
tokenRequired: false,
tokenType: false
}
}
},
localStorage: false,
cookie: true
},
I am using django sessions for my authentication backend, which means that upon a successful login, i will have received a session-id in my response cookie. When i authenticate with nuxt however, i see the cookie in the response, but the cookie is not saved to be used in further requests. Any idea what else i need to be doing?
This is how I handled this, which came from a forum post that I cannot find since. First get rid of nuxt/auth and roll your own with vuex store. You will want two middleware, one to apply to pages you want auth on, and another for the opposite.
This assumes you have a profile route and a login route that returns a user json on successful login.
I'm also writing the user to a cookie called authUser, but that was just for debugging and can be removed if you don't need it.
store/index
import state from "./state";
import * as actions from "./actions";
import * as mutations from "./mutations";
import * as getters from "./getters";
export default {
state,
getters,
mutations,
actions,
modules: {},
};
store/state
export default () => ({
user: null,
isAuthenticated: false,
});
store/actions
export async function nuxtServerInit({ commit }, { _req, res }) {
await this.$axios
.$get("/api/users/profile")
.then((response) => {
commit("setUser", response);
commit("setAuthenticated", true);
})
.catch((error) => {
commit("setErrors", [error]); // not covered in this demo
commit("setUser", null);
commit("setAuthenticated", false);
res.setHeader("Set-Cookie", [
`session=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
`authUser=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
]);
});
}
store/mutations
export const setUser = (state, payload) => (state.user = payload);
export const setAuthenticated = (state, payload) =>
(state.isAuthenticated = payload);
store/getters
export const getUser = (state) => state.user;
export const isAuthenticated = (state) => state.isAuthenticated;
middleware/redirectIfNoUser
export default function ({ app, redirect, _route, _req }) {
if (!app.store.state.user || !app.store.state.isAuthenticated) {
return redirect("/auth/login");
}
}
middleware/redirectIfUser
export default function ({ app, redirect, _req }) {
if (app.store.state.user) {
if (app.store.state.user.roles.includes("customer")) {
return redirect({
name: "panel",
params: { username: app.store.state.user.username },
});
} else if (app.store.state.user.roles.includes("admin")) {
return redirect("/admin/dashboard");
} else {
return redirect({
name: "panel",
});
}
} else {
return redirect("/");
}
}
pages/login- login method
async userLogin() {
if (this.form.username !== "" && this.form.password !== "") {
await this.$axios
.post("/api/auth/login", this.form)
.then((response) => {
this.$store.commit("setUser", response.data);
this.$store.commit("setAuthenticated", true);
this.$cookies.set("authUser", JSON.stringify(response.data), {
maxAge: 60 * 60 * 24 * 7,
});
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect);
}
this.$router.push("/panel");
})
.catch((e) => {
this.$toast
.error("Error logging in", { icon: "error" })
.goAway(800);
The cookie is sent by the server but the client won't read it, until you set the property withCredentials in your client request (about withCredentials read here)
To fix your problem you have to extend your auth config with withCredentials property.
endpoints: {
login: {
url: 'http://localhost:8000/api/login2/',
method: 'post'
withCredentials: true
}
}
Also don't forget to set CORS policies on your server as well to support cookie exchange
Example from ExpressJS
app.use(cors({ credentials: true, origin: "http://localhost:8000" }))
More information about this issue on auth-module github
I am trying authentication using amazon-cognito-vuex-module package
https://github.com/Botre/amazon-cognito-vuex-module
Following is my code for cognito configuration:
export default new Vuex.Store({
modules: {
auth,
cognito: new AmazonCognitoVuexModule({
region: 'us-east-1',
userPoolId: '<myPoolId>',
clientId: '<myClientId>'
})
},
strict: debug
})
and this is how I am trying authenticating user:
this.$store.dispatch('authenticateUser', { email: this.input.username, password: this.input.password })
authenticateUser function is as follows:
authenticateUser ({ commit, state }, payload) {
return new Promise((resolve, reject) => {
commit('setAuthenticating', true)
const email = payload.email
const password = payload.password
const user = new CognitoUser({
Username: email,
Pool: state.pool
})
user.authenticateUser(
new AuthenticationDetails({
Username: email,
Password: password
}),
{
onFailure: function (error) {
commit('setAuthenticating', false)
reject(error)
},
onSuccess: function (session) {
commit('setAuthenticating', false)
commit('setAuthenticated', user)
resolve(session)
},
newPasswordRequired: function (userAttributes, requiredAttributes) {
commit('setAuthenticating', false)
delete userAttributes.email_verified // Immutable field
user.completeNewPasswordChallenge(
payload.newPassword,
userAttributes,
this
)
}
}
)
})
}
Getting following error in response:
{"__type":"NotAuthorizedException","message":"User is not authorized
to get auth details."}