I have a bot in NodeJS connected to Google Chat using HTTPs endpoints. I am using express to receive requests. I need to verify that all requests come from Google, and want to do this using the Bearer Token that Google Sends with requests.
My problem is that I am struggling to find a way to verify the tokens.
I have captured the token and tried a GET reuqes to https://oauth2.googleapis.com/tokeninfo?id_token=ey... (where ey... is the token start).
Which returns:
"error": "invalid_token",
"error_description": "Invalid Value"
}
I have tried what Google recommends:
var token = req.headers.authorization.split(/[ ]+/);
client.verifyIdToken({
idToken: token[1],
audience: JSON.parse(process.env.valid_client_ids)
}).then((ticket) => {
gchatHandler.handleGChat(req.body, res);
}).catch(console.error);
And get the following error:
Error: No pem found for envelope: {"alg":"RS256","kid":"d...1","typ":"JWT"}
Any idea where I should head from here?
Edit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com found this, investigating how to use it. The kid matches the one I get.
Worked it out, eventually.
You need to hit: https://www.googleapis.com/service_accounts/v1/metadata/x509/chat#system.gserviceaccount.com to get a JSON file containing the keys linked to their KIDs.
Then when a request arrives, use jsonwebtoken (NPM) to decode the token and extract the KID from the header.
Use the KID to find the matching public key in the response from the website above, then use the verify function to make sure the token matches the public key.
You also need to pass the audience and issuer options to verify, to validate that it is your particular service account hitting the bot.
The solution above maybe the correct for Google Chat, but in my experience Google services (e.g. Google Tasks) use OIDC tokens, which can be validated with verifyIdToken function.
Adding my solution here, since your question/answer was the closest thing I was able to find to my problem
So, In case if you need to sign a request from your own code
on client, send requests with OIDC token
import {URL} from 'url';
import {GoogleAuth} from 'google-auth-library';
// will use default auth or GOOGLE_APPLICATION_CREDENTIALS path to SA file
// you must validate email of this identity on the server!
const auth = new GoogleAuth({});
export const request = async ({url, ...options}) => {
const targetAudience = new URL(url as string).origin;
const client = await auth.getIdTokenClient(targetAudience);
return await client.request({...options, url});
};
await request({ url: 'https://my-domain.com/endpoint1', method: 'POST', data: {} })
on the server, validate OIDC (Id token)
const auth = new OAuth2Client();
const audience = 'https://my-domain.com';
// to validate
const token = req.headers.authorization.split(/[ ]+/)[1];
const ticket = await auth.verifyIdToken({idToken: token, audience });
if (ticket.getPayload().email !== SA_EMAIL) {
throw new Error('request was signed with different SA');
}
// all good
Read more about Google OpenID Connect Tokens
Related
I have completed the oauth flow for my third party app against a Reddit account and I've gotten the access token for the account.
Now my next issue is
How can I fetch the subreddits for an account using the access token
I can't seem to figure out the endpoint for that.
Does anyone know the endpoint for that?
Thank you
The Reddit OAuth Docs say that for the /subreddits/mine/(where) endpoint, the subreddits OAuth scope is necessary.
Once that scope is acquired for a user, you can use the following snippets of code to access the list of subscribed subreddits for the user:
View a users subreddits
View in Fusebit
// Demonstrate using snooclient and Fusebit
const subscriptions = await redditClient.getSubscriptions().fetchAll();
// OR fetch the first page using a raw HTTP request
// - the User-Agent is necessary, don't forget it!
const access_token = redditClient.fusebit.credentials.access_token;
const httpSubs = await superagent.get(
'https://oauth.reddit.com/subreddits/mine/subscriber')
.set('Authorization', `Bearer ${access_token}`)
.set('User-Agent', 'Fusebit Integration');
const length = httpSubs.body.data.children.length;
ctx.body = {
usingSnoo: `User has ${subscriptions.length} subreddits`,
usingHttp: `The first page has ${length} subreddits`,
};
});
Stack:
Google Sign-in (Vanilla JS - client side),
Firebase Functions (ExpressJS)
Client-Side:
My Firebase function express app uses vanilla javascript on the client side. To authenticate I am making use of Firebase's Google SignIn feature client-side javascript web apps, found here.
// Firebase setup
var firebaseConfig = {
apiKey: "AIza...",
authDomain: "....firebaseapp.com",
databaseURL: "https://...-default-rtdb.firebaseio.com",
...
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
function postIdTokenToSessionLogin(idToken, csrfToken) {
return axios({
url: "/user/sessionLogin", < ----- endpoint code portion found below
method: "POST",
data: {
idToken: idToken,
csrfToken: csrfToken,
},
});
}
// ...
// On sign-in click
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value => {
const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
}).then(value => {
window.location.assign("/user/dashboard")
}).catch((error) => {
alert(error.message);
});
Note I am using value.credential.idToken (most sources imply to use this, but haven't found an example saying use this specifically)
Directly after calling signInWithPopup, a new account is created in my Firebase Console Authentication matching the gmail account that was just signed in.
Server-side:
Once I authenticate, I create an axios request passing in the {user}.credential.idToken and following the server-side setup here (ignoring the CSRF - this just doesn't want to work).
In creating the session, I use the following code in my firebase functions express app, the endpoint which is router.post('/sessionLogin', (req, res) => (part of /user route prefix):
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const idToken = req.body.idToken.toString(); // eyJhbGciOiJSUzI1NiIsImt...[936]
admin
.auth()
.createSessionCookie(idToken, {expiresIn}) < ----------- Problem line
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
On the createSessionCookie call, I get the following error & stack trace:
Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:44:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:90:28)
at new FirebaseAuthError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:149:16)
at Function.FirebaseAuthError.fromServerError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:188:16)
at C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\auth\auth-api-request.js:1570:49
at processTicksAndRejections (internal/process/task_queues.js:93:5)
This is part of the sign-in flow with a existing Gmail account.
What is causing this?
After many hours of searching, Googling - I have seen the light.
For some additional context, this error featured heavily in my struggle "Firebase ID token has invalid signature." - I will get to that in a second.
Further, another issue I also faced was using a local auth emulator for web client-side (javascript), see this for setup.
TL;DR to solve the immediate problem
Client-side remained largely the same, however the documentation provided by Firebase was inaccurate/misleading - thanks to this post, I found the solution. Thus, it follows...
Which is the ID Token? (Client-side):
The examples from here (to allow signInWithPopup), the response (if successful) results in
...
.signInWithPopup(provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Looking for an idToken, I found one using result.credential.idToken but no where on the internet on if this was infact the correct token to use.
I ran into this error using the provided idToken above:
Firebase ID token has incorrect "aud" (audience) claim. Expected
"[insert your **projectId**]" but got
"59895519979-2l78aklb7cdqlth0eob751mdm67kt301.apps.googleusercontent.com".
Make sure the ID token comes from the same Firebase project as the
service account used to authenticate this SDK.
Trying other tokens like result.credential.accessToken responded with various verification errors - what to do?
Mention earlier, this solution on Github suggested to use firebase.auth().currentUser.getIdToken() AFTER you have signed in. An example (building on my previous code) is to do the following:
...
.signInWithPopup(provider)
.then((result) => {
// current user is now valid and not null
firebase.auth().currentUser.getIdToken().then(idToken => {
// send this ID token to your server
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
})
})
At this point, you can verify your token and createSessionCookies to your heart's desire.
BUT, a secondary issue I unknowingly created for myself using the Authentication Emulator.
To setup for client-side use:
var auth = firebase.auth();
auth.useEmulator("http://localhost:9099");
To setup for hosting your firebase functions app (assuming you are using this with e.g. nodejs + express, see this for setup, ask in comments, can provide more details if needed)
Using Authentication Emulator caused the following errors AFTER using the above mentioned "fix". Thus, DO NOT RUN the local authentication emulator (with Google sign-in of a valid Google account) as you will consistently get.
Firebase ID token has invalid signature. See
https://firebase.google.com/docs/auth/admin/verify-id-tokens for
details on how to retrieve an ID token
You can use all your local emulators, but (so far in my experience) you will need to use an online authenticator.
I have OAuth2 server[Spring-boot] which validates the clients with password credentials method.
The access token obtained by the Frontend client[Vuejs] uses the token to access the Resource server[Spring-boot].
Here, while the access token is passed by Frontend client to the resource server, the resource server cross-validated it with the OAuth2 server with the following code
#Configuration
#EnableResourceServer
public class OAuth2ResourceServerConfigRemoteTokenService extends ResourceServerConfigurerAdapter {
#Primary
#Bean
public RemoteTokenServices tokenServices() {
final RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://localhost:9088/oauth/check_token");
tokenService.setClientId("fooClientIdPassword");
tokenService.setClientSecret("password");
return tokenService;
}
}
Now, I m trying to implement the same with nodejs as I planned to split a particular functionality which produces overhead to the resource server written in spring-boot.
I don't know how to implement the cross validation mechanism in nodejs like the below code.
tokenService.setCheckTokenEndpointUrl("https://localhost:9088/oauth/check_token");
try the express-oauth2-bearer library it works fine
first of all install it:
npm i express-oauth2-bearer --save
and use this code in your client :
The library needs the following values to authroize requests:
Issuer Base URL: The base URL of the authorization server. If you're using Auth0, this is your tenant Domain pre-pended with https:// (like https://tenant.auth0.com) found on the Settings tab for your Application in the Auth0 dashboard.
Allowed Audiences: Audience identifier (or multiple separated by a comma) allowed for the access token. If you're using Auth0, this is the Identifier found on the Settings tab for your API in the Auth0 dashboard.
These can be configured in a .env file in the root of your application:
# .env
ISSUER_BASE_URL=https://YOUR_DOMAIN
ALLOWED_AUDIENCES=https://api.yourapplication.com
... or in your application code:
app.use(auth({
issuerBaseURL: 'https://tenant.auth0.com',
allowedAudiences: 'https://api.yourapplication.com'
}));
The OpenID strategy is the default strategy for token validation. With the configuration values set in the .env file, the following code will restrict requests to all proceeding routes to ones that have a valid access token with the https://api.yourapplication.com audience and the read:products scope:
const { auth, requiredScopes } = require('express-oauth2-bearer');
app.use(auth());
app.get('/products',
requiredScopes('read:products'),
(req, res) => {
console.dir(req.auth.claims);
res.sendStatus(200);
});
If access tokens are not expected to be signed like OpenID Connect ID tokens, add the auth middleware with a callback to validate as follows:
const { auth, requiredScopes } = require('express-oauth2-bearer');
const validateAccesToken = async (token) => {
const token = await db.tokens.find(token);
if (token.expired) { return; }
return token;
};
app.use(auth(validateAcessToken)));
app.get('/products',
requiredScopes('read:products'),
(req, res) => {
console.dir(req.auth.claims);
res.sendStatus(200);
});
I have Google Sign-in working on my app: the relevant code is roughly:
var acc = await signInService.signIn();
var auth = await acc.authentication;
var token = auth.idToken;
This gives me a nice long token, which I then pass to my backend with an HTTP POST (this is working fine), and then try to verify. I have the same google-services.json file in my flutter tree and on the backend server (which is nodejs/restify). The backend code is roughly:
let creds = require('./google-services.json');
let auth = require('google-auth-library').OAuth2Client;
let client = new auth(creds.client[0].oauth_client[0].client_id);
. . .
let ticket = await client.verifyIdToken({
idToken: token,
audience: creds.client[0].oauth_client[0].client_id
});
let payload = ticket.getPayload();
This consistently returns my the error "Wrong recipient, payload audience != requiredAudience".
I have also tried registering separately with GCP console and using those keys/client_id instead, but same result. Where can I find the valid client_id that will properly verify this token?
The problem here is the client_id that is being used to create an OAuth2Client and the client_id being used as the audience in the verifyIdToken is the same. The client_id for the audience should be the client_id that was used in your frontend application to get the id_token.
Below is sample code from Google documentation.
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
verify().catch(console.error);
And here is the link for the documentation.
Hope this helps.
Another quick solution might be change the name of your param "audience" to "requiredAudience". It works to me. If you copied the code from google, maybe the google documentation is outdated.
client.verifyIdToken({
idToken,
requiredAudience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
It has already been mentioned above that requiredAudience works instead of audience, but I noticed requiredAudience works for both {client_id : <CLIENT_ID>} and <CLIENT_ID>. So maybe you were referencing creds.client[0].oauth_client[0] instead of creds.client[0].oauth_client[0].client_id? I have not been able to find any docs on the difference between requiredAudience and audience, however make sure you are sending just the <CLIENT_ID> instead of {client_id : <CLIENT_ID>}.
Google doc: link
verifyIdToken()'s call signature doesn't require the audience parameter. That's also stated in the changelog. So you can skip it, and it'll work. The documentation is kinda misleading on that.
It's also the reason why using requiredAudience works because it actually isn't being used by the method, so it's the same as not providing it.
I've been faceing this issue with google-auth-library version 8.7.0 and came across a workaround only if you have a single CLIENT_ID to verify.
Once you create your OAuth2Client like this:
const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
You don't need to pass the CLIENT_ID in verifyIdToken function as it uses your googleClient object to create auth url.
I am having an issue with validating the JWT on the server side end of my node/express app. The token is being generated in Identity Server in an asp.net core app. The token that is generated is an RS256 token type, which means a private key and public key need to be generated on creation in the Identity Server. I need to retrieve a valid certificate with a valid signature.
On the client side (Angular) I'm passing in the Bearer token on all requests once signed in. I need to authenticate that token somehow. The way to do that with a RS256 token type is to make sure the public key matches. I'm using
const jwt2 = require('jwt-simple');
For my JWT validation.
The issue is the secret, here is the jwt-simple documentation jwt-simple link. If I make the third value in decode false it works, because it's ignoring the secret/cert that is required.
I'm making this validation in the middleware so all endpoints will hit it.
I saw this issue - SO Similar Issue and ran those same commands. I'm still getting the error because the token doesn't really have anything to do with the certs because I'm getting it from the Identity Server project. So I need to retrieve the cert public key from that project.
How would I be able to send that cert in the token or retrieve that valid cert somehow?
v1 - (using the self signed server.crt as the cert and getting this error)
Error: Signature verification failed
App.js
//This is for a self-signed certificate locally with no correlation to the token itself.
const options = {
key: fs.readFileSync('./key.pem', 'utf8'),
cert: fs.readFileSync('./server.crt', 'utf8')
};
app.use((req, res, next) => {
if(!req.headers.authorization){
return res.status(403).json({ error: 'No credentials sent!'});
} else {
let token = req.headers.authorization.split(' ')[1]
var decoded = jwt.decode(token, options.cert);
if(decoded){
let currentTime = new Date().getTime()/1000
if(decoded.exp <= currentTime){
return res.status(403).json({
error: 'Token has expired'
});
}
}
else if(!decoded){
return res.status(403).json({
error: 'invalid token'
});
}
}
next();
})
JWT.io parsed token structure -
Header
{
"alg": "RS256",
"kid": "1231231231231231231",
"typ": "JWT",
"x5t": "si7bdXd6......HnxhO4Wi_s"
}
Do I do anything with x5t? Apologies for the long post. Thanks.
If the signing public key is provided with the token and you blindly trust it, it basically defeats the purpose of having signed tokens.
You need some other mechanism for sharing the authentication service's public key. Depending on your requirements for key rotations and how your app works in general, you can either get it from a static path/url when your app starts up, or you may want to build in a mechanism to periodically check for updated valid public key(s).