I stitched together a lot of tutorials and documentation in order to get an access token with MSALin my JavaScript code. Here are the results of my research.
npm install #azure/msal
import the necessary class from #azure/msal
import {
UserAgentApplication,
AuthenticationParameters,
Configuration,
} from "#azure/msal";
Make the msal object
const config: Configuration = {
auth: {
clientId: <client id - your app's client id>,
authority: `https://login.microsoftonline.com/<tenantid>`,
redirectUri: <the redirect Uri>,
},
};
const params: AuthenticationParameters = {
authority: `https://login.microsoftonline.com/${Tenantid}`,
scopes: [`${AppIDUri}/user_impersonation`], <-- the API that you're trying to call
};
const myMSAL = new UserAgentApplication(config);
Get access token
try {
const login = await myMSAL.acquireTokenSilent(params);
return login.accessToken;
} catch (error) {
await myMSAL.loginPopup(params);
const login = await myMSAL.acquireTokenSilent(params);
return login.accessToken;
}
References:
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens
Azure/Msal authentication inside PowerApp Component Framework returns AADSTS50177 error
Related
I'm working on a project where we currently use Cognito User pools for auth., but after some research we found that if we want more fine-grained access-control we should use an Identity pool instead.
The theory is simple : first we create an Identity Pool that uses the Cognito user pool as Auth provider. Then in API Gateway we set up our Lambda to use Authorizer: AWS_IAM. To access it, User now has to :
Sign in to User pool, which gives user a JWT Token.
Exchange that JWT Token with the Identity pool for temporary AWS Credentials.
Use those new credentials to sign API request to the protected Lambda.
Steps 1 and 2 work fine, with a test user we manage to get the JWT Token and successfully exchange it for AWS credentials. They look like this (modified for security reasons):
awsAccessKey: ASIAZFDXSW29NWI3QZ01
awsSecretKey: B+DrYdPMFGbDd1VRLSPV387uHT715zs7IsvdNnDk
awsSessionToken: IQoJb3JpZ2luX2VjEA8aCWV1LXdlc3QtMyJHMEUCIQC4kHasZrfnaMezJkcPtDD8YizZlKESas/a5N9juG/wIQIgShWaOIgIc4X9Xrtlc+wiGuSC1AQNncwoac2vFkpJ3gkqxAQIWBAAGgw2NTI5NTE0MDE0MDIiDDuTZ1aGOpVffl3+XCqhBDmjCS3+1vSsMqV1GxZ96WMoIoEC1DMffPrBhc+NnBf94eMOI4g03M5gAm3uKAVCBkKO713TsQMaf4GOqqNemFC8LcJpKNrEQb+c+kJqqf7VWeWxveuGuPdHl1dmD2/lIc8giY0+q4Wgtbgs6i0/gR5HzdPfantrElu+cRNrn/wIq4Akf+aARUm14XsIgq7/1fT9aKSHpTgrnTLHeXLKOyf/lZ947XdH71IHDZXBUdwdPikJP/Rikwill6RRTVw7kGNOoacagCmmK7CD6uh9h0OnoW3Qw5df+zX5Z8U7U55AyQfEyzeB7bW3KH65yJn6sopegxIIFfcG2CLIvtb5cZYImAz/4BdnppYpsrEgLPUTvRAXn6KUa5sXgc5Vd7tJeRo5qpYckrR2qfbebsU+0361BCYK2HxGJqsUyt1GVsEoAosxofpn/61mYJXqfeR0ifCAgL7OMOquvlaUVXhHmnhWnUSIOUQ+XtRc+DxUDjwn5RPD7QTwLHIat7d4BI4gZJPAcMT9gZrBVO/iN88lk5R0M5LBzFwd5jiUW46H/G755I4e5ZHaT1I37TY3tbcObIFGVVNz5iHDpK/NePTJevKTshe8cYxXczOQgos4J/RsNpqouO9qRgT9JDyXjU3Etyxqm9RzbLYgV3fl5WwZl5ofVmrBsy3adq+088qEz5b9cogPgDggA/nQaPv7nAZHT8u0ct/hw230pmXUDGCutjOML2G6ZYGOoUCy+BitAN0SZOYWlbZlYomIGKMNQuXjV4z+S9CEW8VunqW4Rgl7rTba6xbI0DdX9upYEczeln6pTl+2UPEDYf6usayFfMsGDvJXesqC5EOtWco1Z8tem/wDQIH7ZbioQHZ7UJDd5ntUAruFveY7sXmKsQbtah/RB5W5HLYy19hCmyGpYMnVXxR0FcNGImsweNcprtw9MmQqy2SUK9V6Rwn1yIE6svfAT3NVyzp9ILbP/qSQLGHNhm4CNd8+EJZZa9rcmCbQiQ+iBJ8FW+AmRSCC4LiB1dhuH1KsFo88DyNhYdVf3py8XV4CDR7l+UyuZMrIQsERwx9JzwVBjfv9COT948mvyGTY
The issue is the signing. Our Lambda is behind a CloudFront proxy + API Gateway. Requests to e.g john.dev.project.io are forwarded to the 'real' API origin at api.dev.project.io.
Using Postman and setting AWS Signature, the request doesn't work and gives following error :
The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'................................................................................................................................................................................................................................................................'\n\nThe String-to-Sign should have been\n'............................................................................'\n
We found however, that by overriding the Host header to the real origin of the API, request now works fine :
So it seems that since the custom URL we use and the original API URL are different, signatures don't match. The problem is that by default browsers don't allow you to override Host header for security reasons, so our front-end signed requests always fail.
Maybe the proxy is also modifying other headers before forwarding to origin, which would also invalidate the signature from my understanding...
Any help appreciated in solving this issue!
I was facing a similar issue when trying to make a signed request to an API Gateway endpoint behind an Akamai proxy.
The trick to solve it was indeed to generate a request as if you were sending it directly to the API Gateway URL, sign that request using sigv4 and then send that signed request to the proxy endpoint instead.
I've put together a simple NodeJS code to exemplify how to do this:
const AWS = require("aws-sdk");
const { HttpRequest } = require("#aws-sdk/protocol-http");
const { SignatureV4 } = require("#aws-sdk/signature-v4");
const { NodeHttpHandler } = require("#aws-sdk/node-http-handler");
const { Sha256 } = require("#aws-crypto/sha256-browser");
const REGION = "ca-central-1";
const PROXY_DOMAIN = "proxy.domain.com";
const PROXY_PATH = "/proxypath";
const API_GATEWAY_DOMAIN = "API-ID.execute-api.ca-central-1.amazonaws.com";
const API_GATEWAY_PATH = "/apigateway/path";
const IDENTITY_ID = "{{identity-pool-region}}:{{identity-pool-id}}";
const POOL_REGION = "{{identity-pool-region}}";
const REQUEST_BODY = { test: "test" };
const METHOD = "POST";
const udpatedSignedRequestExample = async () => {
try {
const BODY = JSON.stringify(REQUEST_BODY);
const request = new HttpRequest({
body: BODY,
headers: {
"Content-Type": "application/json",
host: API_GATEWAY_DOMAIN,
},
hostname: API_GATEWAY_DOMAIN,
port: 443,
method: METHOD,
path: API_GATEWAY_PATH,
});
console.log("request", request);
const credentials = await getCredentials();
console.log(credentials);
const signedRequest = await signRequest(request, credentials);
console.log("signedRequest", signedRequest);
const updatedSignedRequest = updateRequest(signedRequest);
console.log("updatedSignedRequest", updatedSignedRequest);
const response = await makeSignedRequest(updatedSignedRequest);
console.log(response.statusCode + " " + response.body.statusMessage);
} catch (error) {
console.log(error);
}
};
const getCredentials = async () => {
var cognitoidentity = new AWS.CognitoIdentity({ region: POOL_REGION });
var params = {
IdentityId: IDENTITY_ID,
};
const response = await cognitoidentity
.getCredentialsForIdentity(params)
.promise();
return {
accessKeyId: response.Credentials.AccessKeyId,
secretAccessKey: response.Credentials.SecretKey,
sessionToken: response.Credentials.SessionToken,
expiration: response.Credentials.Expiration,
};
};
const signRequest = async (request, credentials) => {
const signer = new SignatureV4({
credentials: credentials,
region: REGION,
service: "execute-api",
sha256: Sha256,
});
const signedRequest = await signer.sign(request);
return signedRequest;
};
const updateRequest = (httpRequest) => {
httpRequest.hostname = PROXY_DOMAIN;
httpRequest.path = PROXY_PATH;
httpRequest.headers.host = PROXY_DOMAIN;
return httpRequest;
};
const makeSignedRequest = async (httpRequest) => {
const client = new NodeHttpHandler();
const { response } = await client.handle(httpRequest);
return response;
};
udpatedSignedRequestExample();
Hope that helps.
I have employed the Login with Google functionality in my React app. I am getting the jwt but there is no access token included in the jwt which I need for sending it to the backend (Laravel). On the backend I use Socialite and I want to get the user back with the access token. Right now I am verifying the user with jwt which is not working.
React Code.
const handleGoogleCallbackResponse = (response) => {
signinWithGoogle(response.credential)
}
const signinWithGoogle = async (jwt) => {
try {
const res = await axios.post("/api/users/loginwithgoogle", {jwt: jwt})
console.log("Google data from backend: ", res.data);
} catch (error) {
console.log("Error at signinWithGoogle : ", error);
}
}
useEffect(() => {
/* global google */
google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: handleGoogleCallbackResponse
})
google.accounts.id.renderButton(document.getElementById("google-btn"), {theme: "outline", size: "large"})
}, [])
Backend:
$user = Socialite::driver('google')->stateless()->userFromToken($request->jwt);
I would like to restrict my GraphQL API with User Authentication and Authorization.
All Keystone.JS documentation is talking about AdminUI authentication, which I'm not interested in at the moment.
Facts:
I want to have some social logins (no basic email/password)
I want to use JWT Bearer Tokens
Other than that you can suggest any possible way to achieve this.
My thoughts were:
I could have Firebase Authentication (which can use Google Sign-in, Apple Sign-in etc.) be done on the client-side (frontend) which would then upon successful authentication somehow connect this to my API and register user (?).
Firebase client SDK would also fetch tokens which I could validate on the server-side (?)
What is troubling is that I can't figure out how to do this in a GraphQL environment, and much less in a Keystone-wrapped GraphQL environment.
How does anyone do basic social authentication for their API made in Keystone?
Keystone authentication is independent of the Admin-UI. If you are not restricting your list with proper access control the authentication is useless. Default access is that it is open to all.
you can set default authentication at keystone level which is merged with the access control at list level.
Admin Ui Authentication
Admin UI only supports password authentication, meaning you can not go to /admin/signin page and authenticate there using other authentication mechanism. The Admin Ui is using cookie authentication. cookies are also set when you login using any other login method outside of admin-ui. This means that you can use any means of authentication outside of admin-ui and come back to admin ui and you will find yourself signed in.
Social Authentication:
Social authentication is done using passportjs and auth-passport package. there is documentation to make this work. Single Step Account Creation example is when you create user from social auth automatically without needing extra information (default is name and email). Multi Step Account Creation is when you want to capture more information like preferred username, have them accept the EULA or prompt for birthdate or gender etc.
JWT
I dont believe Keystone does pure JWT, all they do is set keystone object id in the cookie or the token is a signed version of item id (user item id) which can be decrypted only by the internal session manager using cookie secret.
Using Firebase to authenticate user
this is the flow of authentication after you create a custom mutation in keystone graphql.
client -> authenticate with Firebase -> get token -> send token to server -> server verifies the token with firebase using admin sdk -> authenticate existing user by finding the firebase id -> or create (single step) a user or reject auth call (multi step) and let client send more data like age, gender etc. and then create the user -> send token
here is the example of phone auth I did, you can also use passport based firebase package and implement your own solution.
keystone.extendGraphQLSchema({
mutations: [
{
schema: 'authenticateWithFirebase(token: String!): authenticateUserOutput',
resolver: async (obj, { token: fireToken }, context) => {
const now = Date.now();
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors || !data.firebaseUser || !data.firebaseUser.length) {
console.error(errors, `Unable to find user-authenticate`);
throw errors || new Error('unknown_user');
}
const item = data.firebaseUser[0];
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
{
schema: 'signupWithFirebase(token: String!, name: String!, email: String): authenticateUserOutput',
resolver: async (obj, { token: fireToken, name, email }, context) => {
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors) {
throw errors;
}
if (data.firebaseUser && data.firebaseUser.length) {
throw new Error('User already exist');
}
const { errors: signupErrors, data: signupData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createUser($data: UserCreateInput){
user: createUser(data: $data) {
id
name
firebaseId
email
phone
}
}`,
variables: { data: { name, phone: phone, firebaseId: uid, email, wallet: { create: { walletId: generateWalletId() } }, cart: { create: { lineItems: { disconnectAll: true } } } } },
});
if (signupErrors || !signupData.user) {
throw signupErrors ? signupErrors.message : 'error creating user';
}
const item = signupData.user;
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
],
})
I am trying to get an access token for accessing the Firebase Hosting API from a Service account, as described here.
The code below does not return an access_token, but an id_token instead, which fails to authenticate when trying to use the API.
What am I doing wrong? How can I obtain an access token?
const { google } = require("googleapis");
var serviceAccount = require("../functions/src/services/serviceAccountKey.json");
async function getAccessToken() {
try {
const jwtClient = new google.auth.JWT(
serviceAccount.client_email,
null,
serviceAccount.private_key,
["firebasehosting.googleapis.com"],
null
);
const credentials = await jwtClient.authorize();
console.log(credentials);
} catch (error) {
console.log(error);
}
}
getAccessToken();
It returns a credentials object:
{
access_token: undefined,
token_type: 'Bearer',
expiry_date: undefined,
id_token: '...', // edited out
refresh_token: 'jwt-placeholder'
}
For the record, I finally got it.
My token scope was invalid: I should use https://www.googleapis.com/auth/firebase
The valid scopes are listed here
I followed the example here https://stormpath.com/blog/the-ultimate-guide-to-mobile-api-security
and here to acquire an access token
https://support.stormpath.com/hc/en-us/articles/225610107-How-to-Use-Stormpath-for-Token-Management
"use strict";
import { ApiKey } from 'stormpath';
import { Client } from 'stormpath';
let apiKey = new ApiKey(process.env.STORMPATH_API_KEY_ID,
process.env.STORMPATH_API_KEY_SECRET);
let spClient = new Client({apiKey: apiKey });
spClient.getApplication(process.env.STORMPATH_APPLICATION_HREF,
function(err, app) {
var authenticator = new OAuthAuthenticator(app);
authenticator.authenticate({
body: {
grant_type: 'password',
username: username,
password : password
}
}, function (err, result) {
if (!err) console.log(err);
res.json(result.accessTokenResponse);
});
});
I was able to acquire a access_token. I use this token to hit my api with Header Authorization Bearer {access_token}
However, when i put in the middleware stormpath.apiAuthenticationRequired, i keep getting this warning and my api is returned with 401
(node:57157) DeprecationWarning: JwtAuthenticator is deprecated, please use StormpathAccessTokenAuthenticator instead.