We are using the AWS Cognito users pool for our signup/sign-in process which is having the app secret key and client id set in the user pool setting on the AWS user pool.
in our codebase, we are using next-auth for Login and creating sessions with Cognito and use info for the user in session, now the next auth needs the client id and secret key for login into AWS, tried sending empty secret key nexauth give error
https://next-auth.js.org/errors#oauth_callback_error invalid_client {
error: {
message: 'invalid_client',
stack: 'OPError: invalid_client\n' +
' at processResponse (/home/ubuntu/code/node_modules/openid-client/lib/helpers/process_response.js:38:13)\n' +
' at Client.grant (/home/ubuntu/code/node_modules/openid-client/lib/client.js:1338:22)\n' +
' at processTicksAndRejections (internal/process/task_queues.js:95:5)\n' +
' at async Client.callback (/home/ubuntu/code/node_modules/openid-client/lib/client.js:472:24)\n' +
' at async oAuthCallback (/home/ubuntu/code/node_modules/next-auth/core/lib/oauth/callback.js:112:16)\n' +
' at async Object.callback (/home/ubuntu/code/node_modules/next-auth/core/routes/callback.js:50:11)\n' +
' at async NextAuthHandler (/home/ubuntu/code/node_modules/next-auth/core/index.js:133:28)\n' +
' at async NextAuthNextHandler (/home/ubuntu/code/node_modules/next-auth/next/index.js:20:19)\n' +
' at async /home/ubuntu/code/csm-brand-portal-front/next/index.js:56:32\n' +
' at async Object.apiResolver (/home/ubuntu/code/node_modules/next/dist/server/api-utils.js:102:9)',
name: 'OPError'
},
providerId: 'cognito',
message: 'invalid_client'
}
[next-auth][error][CALLBACK_OAUTH_ERROR]
when we pass the secret key it works fine and does its stuff.
now we are trying to add an e2e test case in cypress for our codebase and capture the scenarios, we are trying to add a test for login and go to the user dashboard.
for login into AWS, we are using AWS amplify package in our cypress repo and now the issue with this is it does not support the secret key and API gives an error.
enter image description here
Related
I want to access Google reseller api to get customers and subscriptions using google service account key but not able to do it. Below is my code snippet:
async function runSample() {
const auth = new google.auth.GoogleAuth({
keyFile: "../server/credentials/serviceAccountKey.json",
scopes: ["https://www.googleapis.com/auth/apps.order",
"https://www.googleapis.com/auth/apps.order.readonly"
],
});
// Acquire an auth client, and bind it to all future calls
const authClient = await auth.getClient();
google.options({ auth: authClient });
// Do the magic
const res = await reseller.subscriptions.list();
console.log(res.data);
}
runSample().catch(console.error);
Here I want to get list of the subscription from google reseller console. I referenced above code from google documentation. Here I am getting the error 'Authenticated user is not authorized to perform this action.' and reason given is 'Insufficient permissions'.
errors: [
{
message: 'Authenticated user is not authorized to perform this action.',
domain: 'global',
reason: 'insufficientPermissions'
}
]
If I try to access cloud channel service api I can using the same service account key but it is giving error for reseller api.
I have given service account the owner, cloud workstation admin and service account admin role access.
I have also added scopes in domain wide delegation(dwd).
What else permission do I need?
In order to use a service account it must first be configured though your google workspace account Create a service account
You must also denote in your code the name of the user who your service account has been configured to impersonate.
const auth = new google.auth.GoogleAuth({
keyFile: "../server/credentials/serviceAccountKey.json",
clientOptions: {
subject: 'user#yourdomain.com'
},
scopes: ["https://www.googleapis.com/auth/apps.order"
],
});
I am working on a serverless project using node.js and AWS Lambda.
For auth, I am using AWS Cognito. (Frontend is a web-app in Vue.js on AWS Amplify).
I would like to write my own implementation of resetting a user's password who has forgotten their password.
Basically, the end-user fills up a form with their email. If email is in the system, I send them a reset link (which has a unique code I set in the DB).
I am aware of Cognito's Forgot Password flow and also a solution in which I can capture Cognito's "email sending" code and over-ride the email with my own template passing the code in the URL mentioned here.
I stumbled upon the adminSetUserPassword API which I was sure would work -- but no matter what I do, my lambda function does not get permissions to execute this operation.
This is my nodejs code:
import AWS from 'aws-sdk';
const COGNITO_POOL_ID = process.env.COGNITO_USERPOOL_ID;
const csp = new AWS.CognitoIdentityServiceProvider();
export async function resetUserPassword(username, newPassword) {
// Constructing request to send to Cognito
const params = {
Password: newPassword,
UserPoolId: COGNITO_POOL_ID,
Username: username,
Permanent: true,
};
await csp.adminSetUserPassword(params).promise();
return true;
}
This is my IAM permission for the lambda function (it is in serverless yml format):
CognitoResetPasswordIAM:
Effect: Allow
Action:
- cognito-idp:*
Resource:
- arn:aws:cognito-idp:us-east-1::*
(I will fine-tune the permissions once this works)
The following is the error message I am getting.
I am starting to feel that my approach to doing this is not the recommended way of doing things.
User: arn:aws:sts::[XXXXXXX]:assumed-role/[YYYYYYYYY]-us-east-1-lambdaRole/web-app-service-dev-resetPassword is not authorized to perform: cognito-idp:AdminSetUserPassword on resource: arn:aws:cognito-idp:us-east-1:[[XXXXXXX]]:userpool/us-east-1_ZZZZZZZZ
(Serverless has access to my AWS Access key with * permissions on * resources -- so I don't think I am missing any permissions there).
My questions:
Is this the recommended way of doing this?
Is it possible for me to configure permissions in a way that my lambda functions have the required permissions to perform this operation?
It turns out, you need to use the Amplify API and not the Cognito API.
This involves a couple of steps:
1. Configure your Cognito Amplify Service for Auth.
import Amplify, { Auth } from 'aws-amplify';
export function configureCognitoAuth() {
Amplify.configure({
Auth: {
region: process.env.COGNITO_REGION,
userPoolId: process.env.COGNITO_USERPOOL_ID,
mandatorySignIn: false,
userPoolWebClientId: process.env.COGNITO_CLIENT_ID,
authenticationFlowType: 'USER_PASSWORD_AUTH',
oauth: {
domain: process.env.COGNITO_APP_DOMAIN,
scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
responseType: 'code', // or 'token', note that REFRESH token will only be generated when the responseType is code
},
},
});
// You can get the current config object
Auth.configure();
}
2. Call the Auth.forgotPassword service to send the actual password here
import { Auth } from 'aws-amplify';
async function sendUserPasswordResetEmail(event) {
// Any validation checks, rate limits you want to check here, etc.
try {
configureCognitoAuth();
await Auth.forgotPassword(userId);
} catch (error) {
// An error occurred while sending the password reset email
}
}
3. Write a forgotPasswordEmailTrigger Cognito Hook
This replaces the default Cognito Reset password email with your own custom email.
This is also a lamdba method which you need to attach to the Cognito Custom Message trigger (from Cognito > General Settings > Triggers)
My code for this looks like so:
async function forgotPasswordEmailTrigger(event, context, callback) {
// Confirm it is a PreSignupTrigger
if (event.triggerSource === 'CustomMessage_ForgotPassword') {
const { userName } = event;
const passwordCode = event.request.codeParameter;
const resetUrl = `${BASE_URL}/password_reset/${userName}/${passwordCode}`;
let message = 'Your HTML email template goes here';
message = message
.replace(/{{passwordResetLink}}/g, resetUrl);
event.response.emailSubject = 'Email Subject here';
event.response.emailMessage = message;
}
// Return to Amazon Cognito
callback(null, event);
}
The event.request.codeParameter is where the code is returned from Cognito. I think there is a way to change this, but I didn't bother. I use the same code to verify in the next step.
4. Call the forgotPasswordSubmit method from the Amplify Auth service when a password reset request is sent to your backend
When the user clicks the URL, they come to the website and I pick up the code and the userID from the URL (from Step 3) and then verify the code + reset the password like so:
async function resetPassword(event) {
const { token, password, user_id } = event.body;
// Do your validations & checks
// Getting to here means everything is in order. Reset the password
try {
configureCognitoAuth(); // See step 1
await Auth.forgotPasswordSubmit(user_id, token, password);
} catch (error) {
// Error occurred while resetting the password
}
const result = {
result: true,
};
return {
statusCode: 200,
body: JSON.stringify(result),
};
}
When running the command: near keys XXXXXXX.near I'm getting the error below, I'm expecting that near-cli is using the mainnet network by default, but there seems to be an issue, as it says there is no account, but the account clearly exists.
An error occured
TypedError: [-32000] Server error: account XXXXXX.near does not exist while viewing
at JsonRpcProvider.sendJsonRpc (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/providers/json-rpc-provider.js:158:27)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async JsonRpcProvider.query (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/providers/json-rpc-provider.js:66:24)
at async Account.fetchState (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:47:23)
at async Account.state (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:54:9)
at async Near.account (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/near.js:41:9)
at async exports.keys (/usr/local/lib/node_modules/near-cli/index.js:195:19)
at async Object.handler (/usr/local/lib/node_modules/near-cli/utils/exit-on-error.js:39:9) {
[stack]: 'Error: [-32000] Server error: account XXXXX.near does not exist while viewing\n' +
' at JsonRpcProvider.sendJsonRpc (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/providers/json-rpc-provider.js:158:27)\n' +
' at processTicksAndRejections (internal/process/task_queues.js:97:5)\n' +
' at async JsonRpcProvider.query (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/providers/json-rpc-provider.js:66:24)\n' +
' at async Account.fetchState (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:47:23)\n' +
' at async Account.state (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:54:9)\n' +
' at async Near.account (/usr/local/lib/node_modules/near-cli/node_modules/near-api-js/lib/near.js:41:9)\n' +
' at async exports.keys (/usr/local/lib/node_modules/near-cli/index.js:195:19)\n' +
' at async Object.handler (/usr/local/lib/node_modules/near-cli/utils/exit-on-error.js:39:9)',
[message]: '[-32000] Server error: account XXXXXXX.near does not exist while viewing',
type: 'UntypedError',
context: undefined
}
You have to pass the NODE_ENV=mainnet variable with the near-cli to specify which networkId to connect to. This worked for me NODE_ENV=mainnet near keys account.near
I am still struggling with Google's terminology of apis and services but my goal is to have automated functions via aws lambda which act on a G Suite Account (domain?) or more specific on users of this domain.
For now I just want to list all users of that domain. I run this code locally for testing.
What I have done:
I created a service account
I downloaded the json key file which contains the private key, private key id and so on
I enabled G Suite Domain-wide Delegation.
I delegated domain-wide authority to the service account from the GSuite Account
I added the following scopes for the client in the GSuite Admin Console:
https://www.googleapis.com/auth/admin.directory.group
https://www.googleapis.com/auth/admin.directory.user
This is the implementation:
const { google } = require("googleapis");
const auth = new google.auth.GoogleAuth({
keyFile: "credentials.json",
scopes:
"https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/admin/directory/v1, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user",
});
const service = google.admin({ version: "directory_v1", auth });
service.users.list(
{
domain: "my.domain.com",
maxResults: 10,
orderBy: "email",
},
(err, res) => {
if (err) return console.error("The API returned an error:", err.message);
const users = res.data.users;
if (users.length) {
console.log("Users:");
users.forEach((user) => {
console.log(`${user.primaryEmail} (${user.name.fullName})`);
});
} else {
console.log("No users found.");
}
}
);
I am not sure why I have to add the scopes in the GoogleAuth object but I took this from the google documentation.
When I run this I get the following error:
The API returned an error: invalid_scope: Invalid OAuth scope or ID token audience provided.
The Directory API can only be used by admins
A Service account is not an admin
If the service account shall act on behalf on the admin, you need to
enable G Suite Domain-wide Delegation (as you already did)
impersonate the service account as the admin by setting the user to be impersonated
In general, when you are using a service account you need to build the authentication flow, as explained in the documentation, that is you need to create JSON Web Token (JWT) specifying the user to impersonate.
A sample code snippet for Javascript:
const jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
scopes,
user // User who will be impersonated (needs to be an admin)
);
await jwtClient.authorize();
return jwtClient;
According to Parse js guide:
The Parse.User obtained from Parse.User.current() will always be authenticated. If you need to check if a Parse.User is authenticated,
you can invoke the authenticated method. You do not need to check
authenticated with Parse.User objects that are obtained via an
authenticated method.
So when a user Signs up on my web app, get's a verification email which after clicking will direct the user back to the app. Then I call Parse.User.current() which returns the user object but the sessionToken it's empty. The user object looks like this:
ACL: {*: {…}, vIl61voP42: {…}}
createdAt: "2019-11-08T11:47:44.102Z"
emailVerified: true
objectId: "vIl61voP42"
origin: "Web App"
sessionToken: undefined
updatedAt: "2019-11-08T11:50:19.633Z"
username: "dfgg#dfgg.com"
My goal would be to allow the user to access the app without having to manually login and therefor create a session. From Parse docs my understanding was that once we have a user returned from Parse.User.current() would mean this user would be logged in and therefor I could set and save new data into the user object, which is not the case...
For sign up I've a standard Parse sign up as per documentation:
var user = new Parse.User();
user.set("username", "my name");
user.set("password", "my pass");
user.set("email", "email#example.com");
try {
await user.signUp();
// Hooray! Let them use the app now.
} catch (error) {
// Show the error message somewhere and let the user try again.
alert("Error: " + error.code + " " + error.message);
}
Once sign up form it's submitted the user receives a email confirmation link which will redirect them to my application url. This is how my Parse server settings looks like:
const api = new ParseServer ({
...
verifyUserEmails: true,
emailVerifyTokenValidityDuration: 0,
preventLoginWithUnverifiedEmail: true,
customPages: {
// App
passwordResetSuccess: settings["appUrl"] + "/login",
verifyEmailSuccess: settings["appUrl"] + "/dashboard",
linkSendSuccess: settings["appUrl"] + "/email-confirmation",
// Parse
invalidLink: settings["serverUrl"] + "/expired",
choosePassword: settings["serverUrl"] + "/choose_password"
}
...
})
Finally, once the user arrives at my dashboard route from the verification email link I get the current user via await Parse.User.current() which returns me the user object without a session token. Also checked on my local dev db and there's no session being created.
Any idea?