Twilio client.notify returns 403 - next.js

I need to send notifications in my web app (nextjs) when a user sends a message. I followed this but it dowsn't work.
I have the following code:
export const notifyUser = async (_identity: string, _message: string) => {
return await client.notify
.services(config.TWILIO_CHAT_SERVICE_SID)
.notifications.create({ body: _message, identity: [_identity] });
};
I call an api notify-user and I invoke notifyUSer(identity, message)
Identity is the user_id I use to generate the token with the following method:
export const tokenGenerator = (identity: string) => {
// Create an access token which we will sign and return to the client
const token = new AccessToken(
config.TWILIO_ACCOUNT_SID,
config.TWILIO_API_KEY,
config.TWILIO_API_SECRET
);
// Assign the provided identity or generate a new one
token.identity = identity || 'unknown';
if (config.TWILIO_CHAT_SERVICE_SID) {
// Create a "grant" which enables a client to use IPM as a given user,
// on a given device
const chatGrant = new ChatGrant({
serviceSid: config.TWILIO_CHAT_SERVICE_SID,
pushCredentialSid: config.TWILIO_FCM_CREDENTIAL_SID,
});
token.addGrant(chatGrant);
}
if (config.TWILIO_SYNC_SERVICE_SID) {
// Point to a particular Sync service, or use the account default to
// interact directly with Functions.
const syncGrant = new SyncGrant({
serviceSid: config.TWILIO_SYNC_SERVICE_SID || 'default',
});
token.addGrant(syncGrant);
}
// Serialize the token to a JWT string and include it in a JSON response
return {
identity: token.identity,
token: token.toJwt(),
};
};
I expected to get the response that is in the example but I get the following error:
What do you think I did wrong?

Twilio Notify is a different service to Twilio Chat, so you are trying to use incorrect credentials to call the API. You are passing a Chat service sid, not a Notify service sid, and using the chat identity and not the identity of a Notify binding.
If you are trying to send notifications based on chat messages, you should follow this guide on push notification configuration for chat.
If you are trying to send notifications outside of chat using Twilio Notify, follow this guide.

It has something to do with your authentication for notifications. The 403 error is similar to the 401 unauthorized error but the difference is that the 403 error doesn't allow you to re-authenticate and forbids the request. Check through your authentication part.
You can also check the Twilio debugging guide : https://www.twilio.com/docs/usage/troubleshooting/debugging-your-application

Related

Can I get a Google user identify from their access_token?

Implementing Google OAuth in Firebase Cloud Functions.
Everything is working but I have a weird issue. Everything is working, but I don't know how to identify the user to save the tokens to their user object in Firestore.
Using the google API nodejs library, I create an authURL using the OAuth2 client, set the scopes etc, then redirect the user to it. This works great.
const {google} = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
const scopes = [
'https://www.googleapis.com/auth/calendar'
];
const authorizationUrl = oauth2Client.generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: 'offline',
state: 'state_parameter_gets_passed_back',
scope: scopes,
// Enable incremental authorization. Recommended as a best practice.
include_granted_scopes: true
});
console.log('created an authorizationUrl: ' + authorizationUrl);
res.redirect(authorizationUrl);
I then have an https Cloud Function endpoint set as the redirect URL, waiting for the response.
When it comes I get the code and request the tokens. This also works great, until I get to saveUserToken. Who is the user? My Cloud Function is just listening to responses
exports.recieveGoogleCodeFromResponseURL = functions.https.onRequest(async (req, res) => {
const code = req.query.code;
console.log('got a code, it is:' + code);
const url = require('url');
if (req.query.code != null) {
let userCredential;
console.log('we have a code, trading it for tokens');
let { tokens } = await oauth2Client.getToken(code);
console.log({ tokens });
oauth2Client.setCredentials(tokens);
//THIS IS THE PROBLEM HERE, who is the user to save the tokens to?
saveUserToken(tokens, uid); //saves to Firestore
}
res.json({result: `Got a response from Google`, code: code, scope: req.query.scope});
});
The response looks like this:
{
access_token: "longtoken",
expiry_date: 166...,
refresh_token: "anothertoken",
scope: "https://www.googleapis.com/auth/calendar",
token_type: "Bearer"
}
From what I understand neither the access_token or refresh_token is JWT token I could decode to get user info.
All of the Firebase Cloud Functions examples I have read from Google say something like 'In production you would save this token to a secure persistent DB', which I can do with Firestore. I just can't figure out how to ID the user the callback and code belongs to.
All the code samples that show OAuth with other services (Instagram, LinkedIn, Twitch) either the results come with the user id, or their API allows you to query the service with just the access_token and get the user.
For example in this Login with Instagram example the response comes with the user Id.
Code here > https://github.com/firebase/functions-samples/blob/main/instagram-auth/functions/index.js
const oauth2 = instagramOAuth2Client();
const results = await oauth2.authorizationCode.getToken({
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI,
});
functions.logger.log('Auth code exchange result received:', results);
// We have an Instagram access token and the user identity now.
const accessToken = results.access_token;
const instagramUserID = results.user.id;
In this OAuth example from LinkedIn, once again they pass the access token to a LinkedIn endpoint to identify the user. Code here https://github.com/firebase/functions-samples/blob/main/linkedin-auth/functions/index.js
const linkedin = Linkedin.init(results.access_token);
linkedin.people.me(async (error, userResults) => {
if (error) {
throw error;
}
functions.logger.log(
'Auth code exchange result received:',
userResults
);
// We have a LinkedIn access token and the user identity now.
const linkedInUserID = userResults.id;
I can use this Google library to validate an ID token, but I am not getting an ID token back from the OAuth process.
Feels like I am missing something simple. Is there a Google API I can pass an access_token to to identify the user?
The access_token returned by Google OAuth is not a JWT. It's an opaque string that is only meaningful to Google, and that you can use to identify the user with Google APIs.
You can use the Google People API to get information about the user. With the access_token you can query the Google People API and get information about the user
Sh_ghosa's answer sent me down a misleading rabbit hole so I'm providing this answer to protect your time that I've spent for you.
The following code example uses the NodeJS Google Cloud SDK and therefore assumes that the GoogleAuth module is provided in the language you're using.
// libraries (npm)
const {google} = require('googleapis');
const jwt = require('jsonwebtoken');
// business logic
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const authClient = await auth.getClient();
const accessToken = await authClient.getAccessToken();
const decodedJwt = jwt.decode(accessToken?.res?.data?.id_token);
const principal = decodedJwt?.email; // <:: the principal
console.log('Who am I? ', principal);
Found a solution.
Ask for additional scopes
At first I was just asking for Google Calendar permissions.
const scopes = 'https://www.googleapis.com/auth/calendar'
The trick is to ask for email, and profile as well,
const scopes = 'https://www.googleapis.com/auth/calendar email profile'
If you ask for these additional scopes, Google sends back an id_token with the access_token and the refresh_token
You can get the user email and other information from that id_token.
You can decode the token locally since it came from a secure https session with Google and you check that the state variable passed back matches the one your system generated.
function decodeIdTokenLocally(token){
//we split the id_token at the period ., and just decode the 2nd part
let secondPart = token.split('.')[1];
let localUserJsonString = atob(secondPart);
let localUser = JSON.parse(localUserJsonString);
return localUser
}
Quick note - Asking for more than 1 scope at once gives this ugly checkbox interface where users have to manually click the checkboxes of the scope you are asking for.
Better to have them Login With Google first, which grants the email and profile scopes, then ask for Calendar scope or additional scopes one at a time which will present the nice one click to accept interface.
I haven't tried #sh_gosha suggestion of sending the access_token to the Google People API but I think that would work as well, but it would add an additional API call.

How to use my firebase authentication to work with external services?

Ok so I am using firebase as authentication for my iOS app. Now I plan on adding video calling to my app using an external service know as connectyCube. This service has their own authentication system and I cannot use their services unless a user is authenticated.
Option 1: I can use their own authentication which means my app would have two authentication systems - not very productive
Option 2: They say I can use an existing authentication to validate users
I understand that this is a common thing in the developers world and I see the word OAuth and JWT being thrown around but I am a rookie developer and I want to understand how I can use firebase and authenticate a user from an external service.
These are the questions they have asked when I opted for the "I have my own authentication" option:
What is your end point URL
Is it GET or POST
Request Headers
Request Params
Response Params
Where do I get all this information from firebase? Any help would be great
As an alternative to #Dharmaraj's answer, you could instead make use of a HTTP Event Cloud Function for this based on the code sample they've provided.
Using this method, you create the endpoint /verifyUserToken to be used by ConnectyCube.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const verifyUserToken = functions.https.onRequest((req, res) => {
const idToken = req.query.token;
verifyUser(idToken)
.then(
(userData) => {
res.status(200).json(userData)
},
(err) => {
console.log("Token verification failed.", err.code || err.message);
res.status(422).json({error: "User token is invalid"})
}
)
.catch((err) => console.error("Unexpected crash", err));
});
async function verifyUser(token) {
if (!token)
throw new Error("token missing");
// using `true` here to force token to be checked against the Firebase
// Auth API rather than trusting its contents as-is
const { uid, email } = await admin.auth().verifyIdToken(token, true);
// pull the user's username from their user data
// at /users/{userId}/username
const username = (await admin.database().ref("users/" + uid + "/username")).val();
// use user's actual email if available, otherwise fallback
// to a userID based email
const uEmail = email || uid + "#users.noreply.yourapp.com";
// use user's username if available, otherwise fallback to
// the email address above.
const uLogin = username !== null ? username : uEmail;
return {
uid,
login: uLogin,
email: uEmail,
user: {id: uid, login: uLogin, email: uEmail}, // <- this part in particular is used by ConnectyCube
users: [{uid, login: uLogin, email: uEmail}]
};
}
Once deployed, you would use the following settings:
Setting
Value
API URL:
https://us-central1-PROJECT-ID.cloudfunctions.net/verifyUserToken
GET/POST
GET
Request params:
{"token": "#{login}"}
Response params:
{"uid": "#{user.id}", "email": #{user.email}, "login": "#{user.login}"}
It looks like ConnectyCube uses some sort of Session Tokens as mentioned in their documentation with their own username and password.
The most easiest way would be creating a ConnectyCube account whenever a new user signs up in your Firebase app using Firebase Auth Triggers for Cloud functions. Then you can generate username and password on behalf of your user and store them in a Database.
So whenever you need to create a ConnectyCube session, check for the currently logged in user and fetch their ConnectyCube credentials.
async function createCCSession() {
const userId = firebase.auth().currentUser.uid
const ccCrednetials = (await firebase.database().ref(`ccCreds/${userId}`).once('value')).val()
ConnectyCube.createSession(ccCredentials)
.then((session) => {
console.log(session)
return session
}).catch((error) => console.log(error));
}
You can protect the database using security rules so a user can access their credentials only.
{
"rules": {
"ccCreds": {
"$uid": {
".read": "$uid === auth.uid"
}
}
}
}
While I don't normally double-answer a question, in the course of exploring some other authentication related problems, I've managed to eliminate the Cloud Function from my other answer entirely and instead call the Authentication API directly.
Setting
Value
API URL:
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=FIREBASE_CONFIG_API_KEY
GET/POST
POST
Request params:
{"idToken": "#{login}"}
Response params:
{"uid": "#{users.0.localId}", "email": #{users.0.email}, "full_name": "#{users.0.displayName}"}
On your client, you just call the ConnectyCube Login API with the following data:
POST https://api.connectycube.com/login
login=<Firebase-ID-token>
password=<any-random-value-to-pass-the-validation>

Trying to authenticate a service account with firebase-admin from a Cloud Scheduler call? Error: Firebase ID token has incorrect "iss" (issuer) claim

I'm trying to authenticate an API call made to my server (on Cloud Run) from a Cloud Scheduler cron job.
I'm trying to use a service account to this.
Note: this is all happening inside the same Project.
References:
https://cloud.google.com/scheduler/docs/http-target-auth
This is what I'm doing:
STEP 1 - Create the service account
I went to https://console.cloud.google.com/apis/credentials and created a new service account. I've assigned the role as owner.
STEP 2 - Create the cron job.
I went to https://console.cloud.google.com/cloudscheduler to create the cron job like I always do.
In the service account field I've put my service account e-mail. In the Audience field, I've put my project id because at some point I got an error saying that it was expecting it to be the name of my project id.
This was the error:
Firebase ID token has incorrect "aud" (audience) claim. Expected "PROJECT_ID"
STEP 3 - Running the job and identify decoding the token:
This is the code on my server:
import * as admin from "firebase-admin";
admin.initializeApp({
credential: admin.credential.cert(
// THIS IS THE DEFAULT FIREBASE-ADMIN SERVICE ACCOUNT
// THAT IS AUTOMATICALLY CREATED BY FIREBASE
SERVICE_ACCOUNT as admin.ServiceAccount
)});
// THIS IS THE CODE THAT IS INSIDE MY SERVER TRYING TO VERIFY THE SERVICE ACCOUNT
try {
const authHeader = req.headers.authorization;
console.log(`authHeader: ${authHeader}`);
if (authHeader) {
const idToken = authHeader.split(" ")[1]; // GETS THE USER ID TOKEN
const decodedToken = await admin.auth().verifyIdToken(idToken);
console.log(`decodedToken: ${decodedToken}`);
}
}
And this is the error I'm currently getting:
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/"my-project-id" but got "https://accounts.google.com". Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
Is there anything wrong with the way I'm doing it? Should I not be using firebase-admin to this?
Should I be using google-auth-library to verify the token?
https://github.com/googleapis/google-auth-library-nodejs#verifying-id-tokens
https://developers.google.com/identity/sign-in/web/backend-auth
After a morning in hell trying to debug this, here is what I've found.
It seems that firebase-admin's admin.auth().verifyIdToken() only works for tokens generated from the firebase SDK.
I got it to work by using the google-auth-library directly.
I did the following.
NOTE: The rest of the code is the same (using the same service account as described in the question):
import { OAuth2Client } from "google-auth-library";
export const apiExpressRouteHandler: RequestHandler = async (req, res) => {
try {
const PROJECT_ID = process.env.PROJECT_ID;
const authHeader = req.headers.authorization;
if (authHeader) {
const client = new OAuth2Client(PROJECT_ID);
const ticket = await client.verifyIdToken({
idToken: authHeader.split(" ")[1],
audience: PROJECT_ID
});
// LOGGING ticket PROPERTIES
console.log(`userId: ${JSON.stringify(ticket.getUserId())}`);
console.log(`payload: ${JSON.stringify(ticket.getPayload())}`);
console.log(`envelope: ${JSON.stringify(ticket.getEnvelope())}`);
console.log(`attributes: ${JSON.stringify(ticket.getAttributes())}`);
}
// REST OF THE CODE
}
}
catch(err) {
// ...
}
I'm not sure if the PROJECT_ID is necessary to initialize the client with new OAuth2Client(PROJECT_ID); but it is working like this.

How to access classroom api after signing through Firebase using Google Sign-in

I have created a unity application to sign-in using google and access google-classroom api. The sign-in is successful and the scope allows access to the courses too.
Question:
How to query the google classroom api after signing in with firebase.
endpoint : https://classroom.googleapis.com/v1/courses/105459102203
Method : GET
Parameter : CourseId which I already have
BearerToken : How to retrieve this from firebase?
When I try using auth-code and/or idToken it gives the following error:
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
Thanks in advance.
There are many ways to make a succesfully API request via firebase Auth, in particulary to Google Classroom API:
The hard way is to create a HttpInterceptor for the firebase.UserCredentials and pass it on headers of every HttpRequest, somethis like this:
headers: new HttpHeaders(
{
'Content-Type': 'application/json',
Authorization: `Bearer [${this.user$.AccessToken}]`
})
this is what I call the hard way because you have to ensure to pass and refresh tokens y every API services.
Use the javascript library "gapi" to login the client, and then use the token response as credential to login in Firebase. This aproach creates a pure OAuth2 login that serves to Firebase and further Google APIs requests, as follows:
declare var gapi;
/** Initialize Google API Client */
initClient(): void {
gapi.client.init({
apiKey: environment.firebaseConfig.apiKey,
clientId: environment.firebaseConfig.clientId,
discoveryDocs: environment.firebaseConfig.discoveryDocs,
scope: environment.firebaseConfig.scope,
});
}
/** Do a OAuth login and then pass it to a FirebaseAuth service */
async login() {
const googleAuth = gapi.auth2.getAuthInstance();
const googleUser = await googleAuth.signIn();
const token = googleUser.getAuthResponse().id_token;
const credential = firebase.auth.GoogleAuthProvider.credential(token);
await this.afAuth().signInAndRetrieveDataWithCredential(credential);
}
/** Then you're ready to make a request*/
/**
* Lists all course names and ids.
* Print the names of the first 10 courses the user has access to. If
* no courses are found an appropriate message is printed.
*/
listCourses() {
this.courses$ =
gapi.client.classroom.courses.list({pageSize=10;}).then(response => {
return from<Course[]>(response.result.courses);
});
}

firebase auth with MIcrosoft Graph (accessToken)

I am super hopeful someone can help me - I'm kind of stuck.
I'm happily using firebase auth with Microsoft AD. My AuthProvider is firebase.auth.OAuthProvider('microsoft.com').
When I call firebase.auth().signInWithPopup() with that provider, everything works GREAT. I can pick out the accessToken from the resulting UserCredential and access Microsoft Graph api's no problem (yay!).
Firebase persists and renews the authentication and my app gets the expected callback via onAuthStateChanged with the new firebase.User when the user returns to my SPA later (also yay!).
The bad news (where I'm stuck) is: how do I get the Microsoft Graph accessToken in this flow (e.g. when the user returns to my app later)? I don't want them to have to re-authenticate with another popup (yech).
Basically, how do I go from a valid firebase.User to a MS Graph accessToken when the user returns?
Thanks so so much for any help!
Firebase Auth only focuses on authentication only. They will return the OAuth access token on sign in success via UserCredential but will discard the Microsoft OAuth refresh token and not store any OAuth credential associated with the provider. So you have no way to get a new access token afterwards. If you have a good reason for Firebase Auth to manage OAuth access tokens, please file an official feature request.
UPDATE/answer: so it turns out to be simpler than I thought:
The basic idea is to authenticate (re-authenticate) using firebase and use the same clientID for silent microsoft authentication. However, you must supply a loginHint
parameter to the microsoft auth, even if you were previously authorized. loginHint can
be the email address for the firebase user...
In that scenario, the authentication is shared and you won't need to popup a second sign-in for the "microsoft half" of the process - the firebase auth works fine.
I ended up using microsoft's MSAL library (https://github.com/AzureAD/microsoft-authentication-library-for-js)... something like this:
const graphDebug = false;
const msalLogger = new Logger(msalLogCallback, { level: LogLevel.Error });
export async function graphClient(loginHint: string) {
const msal = new UserAgentApplication({
// gotcha: MUST set the redirectUri, otherwise get weird errors when msal
// tries to refresh an expired token.
auth: { clientId: CLIENT_ID, redirectUri: window.location.origin },
system: { logger: msalLogger },
// TODO: should we set cache location to session/cookie?
});
/**
* Create an authprovider for use in subsequent graph calls. Note that we use
* the `aquireTokenSilent` mechanism which works because firebase has already
* authenticated this user once, so we can share the single sign-on.
*
* In order for that to work, we must pass a `loginHint` with the user's
* email. Failure to do that is fatal.
*/
const authProvider: AuthProvider = callback => {
msal
.acquireTokenSilent({ scopes: SCOPES, loginHint })
.then(result => {
callback(null, result.accessToken);
})
.catch(err => callback(err, null));
};
const client = Client.init({
authProvider,
debugLogging: graphDebug,
});
return client;
}
When you are using signInWithPopup, the result object contains the credentials you are looking for.
firebase.auth().signInWithPopup(provider)
.then(function(result) {
// User is signed in.
// IdP data available in result.additionalUserInfo.profile.
// OAuth access token can also be retrieved:
// result.credential.accessToken
// OAuth ID token can also be retrieved:
// result.credential.idToken
})
.catch(function(error) {
// Handle error.
});
Hope this helps.
If you look deep enough you should find msal access token in firebase response under (firebaseAuth.currentUser as zzx).zzj()

Resources