Unable to send notification from an Expo React Native App using Azure Notification Hub REST API - firebase

I am trying to receive notifications in an Expo React Native App.
The notifications will be sent using Azure Notification Hub REST API
I followed the steps below :
Added the Android project in Firebase Console
To get the Server Key I followed - Firebase messaging, where to get Server Key?
Configured the FCM ServerKey in Azure Notification Hub
Added the google-services.json at the root in my React Native App and modified app.json as mentioned in - https://docs.expo.dev/push-notifications/using-fcm/
To register in ANH, we first need the SAS Token - https://learn.microsoft.com/en-us/rest/api/notificationhubs/common-concepts I generated the token with the following code
const Crypto = require('crypto-js');
const resourceURI =
'http://myNotifHubNameSpace.servicebus.windows.net/myNotifHubName ';
const sasKeyName = 'DefaultListenSharedAccessSignature';
const sasKeyValue = 'xxxxxxxxxxxx';
const expiresInMins = 200;
let sasToken;
let location;
let registrationID;
let deviceToken;
function getSASToken(targetUri, sharedKey, ruleId, expiresInMins) {
targetUri = encodeURIComponent(targetUri.toLowerCase()).toLowerCase();
// Set expiration in seconds
var expireOnDate = new Date();
expireOnDate.setMinutes(expireOnDate.getMinutes() + expiresInMins);
var expires =
Date.UTC(
expireOnDate.getUTCFullYear(),
expireOnDate.getUTCMonth(),
expireOnDate.getUTCDate(),
expireOnDate.getUTCHours(),
expireOnDate.getUTCMinutes(),
expireOnDate.getUTCSeconds()
) / 1000;
var tosign = targetUri + '\n' + expires;
// using CryptoJS
//var signature = CryptoJS.HmacSHA256(tosign, sharedKey);
var signature = Crypto.HmacSHA256(tosign, sharedKey);
var base64signature = signature.toString(Crypto.enc.Base64);
//var base64signature = signature.toString(CryptoJS.enc.Base64);
var base64UriEncoded = encodeURIComponent(base64signature);
// construct autorization string
var token =
'SharedAccessSignature sr=' +
targetUri +
'&sig=' +
base64UriEncoded +
'&se=' +
expires +
'&skn=' +
ruleId;
console.log('signature:' + token);
return token;
}
I then called the create registration API - https://learn.microsoft.com/en-us/rest/api/notificationhubs/create-registration-id
The registrationID has to be extracted from the response header of the API Call
I used the following code to generate the ANH Regsitration ID
async function createRegistrationId() {
const endpoint =
'https://xxxxxx.servicebus.windows.net/xxxxxxx/registrationIDs/?api-version=2015-01';
sasToken = getSASToken(resourceURI, sasKeyValue, sasKeyName, expiresInMins);
const headers = {
Authorization: sasToken,
};
const options = {
method: 'POST',
headers: headers,
};
const response = await fetch(endpoint, options);
if (response.status !== 201) {
console.log(
'Unbale to create registration ID. Status Code: ' + response.status
);
}
console.log('Response Object : ', response);
for (var pair of response.headers.entries()) {
//console.log(pair[0] + ': ' + pair[1]);
}
location = response.headers.get('Location');
console.log('Location - ' + location);
console.log('Type - ' + response.type);
registrationID = location.substring(
location.lastIndexOf('registrationIDs/') + 'registrationIDs/'.length,
location.lastIndexOf('?api-version=2015-01')
);
console.log('Regsitration ID - ', registrationID);
return location;
}
Next step was to update this registration ID in ANH with the Native Device Token
I used expo-notifications package and the method getDevicePushTokenAsync() method to get the native device token
async function registerForPushNotificationsAsync() {
let token;
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const {
status
} = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getDevicePushTokenAsync()).data;
console.log(token);
} else {
alert('Must use physical device for Push Notifications');
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token;
}
The native device token was in the following format on Android device
c6RI81R7Rn66kWZ0rar3M2:APA91bEcbLXGwEZF-8hu1yGHfXgWBNuxr_4NY_MR8d7HEzeHAJrjoJnjUlneAIiVglCNIGUr11qkP1G4S76bx_H7NItxfQhZa_bgnQjqSlSaY4-oCoarDYWcY-Mz_ulW8rQZFy_SA6_j
I then called the updateRegistrationId API - https://learn.microsoft.com/en-us/rest/api/notificationhubs/create-update-registration
async function updateRegistraitonId() {
//IF you use registrationIDs as in returned location it was giving 401 error
const endpoint =
'https://xxxxx.servicebus.windows.net/xxxxxxx/registrations/' +
registrationID +
'?api-version=2015-01';
const endpoint1 = location;
const headers = {
Authorization: sasToken,
'Content-Type': 'application/atom+xml;type=entry;charset=utf-8',
};
//Remember to create well-formed XML using back-ticks
//else you may get 400 error
//If you use the tags element it was giving an error
const regDATA = `<entry xmlns="http://www.w3.org/2005/Atom">
<content type="application/xml">
<GcmRegistrationDescription xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/netservices/2010/10/servicebus/connect">
<GcmRegistrationId>${deviceToken}</GcmRegistrationId>
</GcmRegistrationDescription>
</content>
</entry>`;
const options = {
method: 'PUT',
headers: headers,
body: regDATA,
};
const response = await fetch(endpoint, options);
if (response.status !== 201) {
console.log(
'Looks like there was a problem. Status Code: ' + response.status
);
console.log('Response Object : ', response);
//return;
}
}
According to API documentation, I should get 201 response, I got 200 response code . I am not sure if this is the issue
After this I had the notification handling code to recieve the notification,similar to the code in - https://docs.expo.dev/versions/latest/sdk/notifications/
I then tried to send notification using Test Send from ANH, it failed with the error -
**
"The Token obtained from the Token Provider is wrong"
**
I checked in ANH Metrics, the error was categorized as GCM Authentication error, GCM Result:Mismatch SenderId
I tried to check for documentation to add the SenderId , but I couldnt find anyway to inlcude the SenderId also in the payload of updateRegistration call (in xml atom entry)
I tried to use the device token and send directly from Firebase Console, I did not receive it either.
I used the Direct Send API of Azure notification Hub but still did not receive anything
I am suspecting there could be some issue in the way I am handling notifiations in the client device, I can fix that later , but first I will have to resolve the error I am getting in Test Send in Azure NH
Any help to be able to successfully send using Test Send in ANH or pointers ahead for next steps will be much appreciated

Related

How do I make an M-Pesa Callback URL using Firebase Cloud Firestore?

I'm trying to make an app that can send payments to PayBill numbers with Safaricom's "Lipa Na M-Pesa" (a Kenyan thing). The call is a POST request to URL:
https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest
with header:
{
'Host': 'sandbox.safaricom.co.ke',
'Authorization': 'Bearer ${await mpesaAccessToken}',
'Content-Type': 'application/json',
}
and body:
{
"BusinessShortCode": "$businessShortCode",
"Password": "${generateLnmPassword(timeStamp)}",
"Timestamp": "$timeStamp",
"TransactionType": "CustomerPayBillOnline",
"Amount": "10",
"PartyA": "$userPhoneNumber",
"PartyB": "$businessShortCode",
"PhoneNumber": "$userPhoneNumber",
"CallBackURL": "?????????????????????????????",
"AccountReference": "account",
"TransactionDesc": "test",
}
I've received an access token, generated a password and made the call successfully, except for that CallBackURL thing... The M-Pesa docs describe their callback like this:
CallBackURL
This is the endpoint where you want the results of the transaction delivered. Same rules for Register URL API callbacks apply.
all API callbacks from transactional requests are POST requests, do not expect GET requests for callbacks. Also, the data is not formatted into application/x-www-form-urlencoded format, it is application/json, so do not expect the data in the usual POST fields/variables of your language, read the results directly from the incoming input stream.
(More info here, but you may need to be logged in: https://developer.safaricom.co.ke/get-started see "Lipa na M-Pesa")
My app is hosted on Firebase Cloud Firestore. Is there any way I can create a callback URL with them that will receive their callback as a document in a Firestore collection?...
Or would this be impossible, given that they would need authorization tokens and stuff to do so... and I can't influence what headers and body M-Pesa will send?
(PS Btw, I code in Flutter/Dart so plz don't answer in Javascript or anything! I'll be clueless... :p Flutter/Dart or just plain text will be fine. Thanks!)
Is there any way I can create a callback URL with them that will
receive their callback as a document in a Firestore collection?...
The most common way to do that in the Firebase ecosystem is to write an HTTPS Cloud Function that will be called by the Safaricom service.
Within the Cloud Function you will be able to update the Firestore document, based on the content of the POST request.
Something like:
exports.safaricom = functions.https.onRequest((req, res) => {
// Get the header and body through the req variable
// See https://firebase.google.com/docs/functions/http-events#read_values_from_the_request
return admin.firestore().collection('...').doc('...').update({ foo: bar })
.then(() => {
res.status(200).send("OK");
})
.catch(error => {
// ...
// See https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3
})
});
I did note that you ask us to not "answer in Javascript or anything" but in Flutter/Dart, but I don't think you will able to implement that in Flutter: you need to implement this webhook in an environment that you fully control and that exposes an API endpoint, like your own server or a Cloud Function.
Cloud Functions may seem complex at first sight, but implementing an HTTPS Cloud Functions is not that complicated. I suggest you read the Get Started documentation and watch the three videos about "JavaScript Promises" from the Firebase video series, and if you encounter any problem, ask a new question on SO.
Cloud functions are not Dart-based.
See below solution;
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const parse = require("./parse");
admin.initializeApp();
exports.lmno_callback_url = functions.https.onRequest(async (req, res) => {
const callbackData = req.body.Body.stkCallback;
const parsedData = parse(callbackData);
let lmnoResponse = admin.firestore().collection('lmno_responses').doc('/' + parsedData.checkoutRequestID + '/');
let transaction = admin.firestore().collection('transactions').doc('/' + parsedData.checkoutRequestID + '/');
let wallets = admin.firestore().collection('wallets');
if ((await lmnoResponse.get()).exists) {
await lmnoResponse.update(parsedData);
} else {
await lmnoResponse.set(parsedData);
}
if ((await transaction.get()).exists) {
await transaction.update({
'amount': parsedData.amount,
'confirmed': true
});
} else {
await transaction.set({
'moneyType': 'money',
'type': 'deposit',
'amount': parsedData.amount,
'confirmed': true
});
}
let walletId = await transaction.get().then(value => value.data().toUserId);
let wallet = wallets.doc('/' + walletId + '/');
if ((await wallet.get()).exists) {
let balance = await wallet.get().then(value => value.data().moneyBalance);
await wallet.update({
'moneyBalance': parsedData.amount + balance
})
} else {
await wallet.set({
'moneyBalance': parsedData.amount
})
}
res.send("Completed");
});
Parse function.
const moment = require("moment");
function parse(responseData) {
const parsedData = {};
parsedData.merchantRequestID = responseData.MerchantRequestID;
parsedData.checkoutRequestID = responseData.CheckoutRequestID;
parsedData.resultDesc = responseData.ResultDesc;
parsedData.resultCode = responseData.ResultCode;
if (parsedData.resultCode === 0) {
responseData.CallbackMetadata.Item.forEach(element => {
switch (element.Name) {
case "Amount":
parsedData.amount = element.Value;
break;
case "MpesaReceiptNumber":
parsedData.mpesaReceiptNumber = element.Value;
break;
case "TransactionDate":
parsedData.transactionDate = moment(
element.Value,
"YYYYMMDDhhmmss"
).unix();
break;
case "PhoneNumber":
parsedData.phoneNumber = element.Value;
break;
}
});
}
return parsedData;
}
module.exports = parse;

Any API to add an authorized domain to Firebase Auth?

Just want to check, is there any API to add the authorized domain in a programmatical way instead of adding it manually by going to Firebase console?
Also, is there any limit on how many domains can be added as the authorized domains?
JavaScript in Cloud Functions solution
import { google } from "googleapis";
(async () => {
/**
* ! START - Update Firebase allowed domains
*/
// Change this to whatever you want
const URL_TO_ADD = "engineering.acme-corp.net";
// Acquire an auth client, and bind it to all future calls
const auth = new google.auth.GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
const authClient = await auth.getClient();
google.options({ auth: authClient });
// Get the Identity Toolkit API client
const idToolkit = google.identitytoolkit("v3").relyingparty;
/**
* When calling the methods from the Identity Toolkit API, we are
* overriding the default target URLs and payloads (that interact
* with the v3 endpoint) so we can talk to the v2 endpoint, which is
* what Firebase Console uses.
*/
// Generate the request URL
const projectId = await auth.getProjectId();
const idToolkitConfigUrl = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
// Get current config so we can use it when we later update it
const currentConfig = await idToolkit.getProjectConfig(undefined, {
url: idToolkitConfigUrl,
method: "GET",
});
// Update the config based on the values that already exist
await idToolkit.setProjectConfig(undefined, {
url: idToolkitConfigUrl,
method: "PATCH",
params: { updateMask: "authorizedDomains" },
body: JSON.stringify({
authorizedDomains: [
...(currentConfig.data.authorizedDomains || []),
URL_TO_ADD,
],
}),
});
})();
A quick note on other languages
The principles should be the same:
Find a way to interact with Google's identify toolkit API (maybe Google offers an SDK to your language)
Get current config
Set new config
If you can't find an SDK, you can also work with raw http requests: https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/getConfig (it's just a bit trickier to do authentication when doing everything manually)
There is no API for this - you must do it through the console. You can also file a feature request with Firebase support if you want.
There doesn't appear to be any documentation stating limits of number of domains. Again, reach out to Firebase support if the documentation is unclear.
Thanks #Jean Costa
Totally working for me.
Here is C# implementation
using Google.Apis.Auth.OAuth2;
using Newtonsoft.Json;
var serviceAccountJsonFile = "path to service account json";
var projectId = "your project ids";
var authorizedDomains = new
{
authorizedDomains = new string[] {
"localhost",
"******.firebaseapp.com",
"*********.web.app",
"abc.def.com"
}
}; // your desire authorized domain
List<string> scopes = new()
{
"https://www.googleapis.com/auth/identitytoolkit",
"https://www.googleapis.com/auth/firebase",
"https://www.googleapis.com/auth/cloud-platform"
};
var url = "https://identitytoolkit.googleapis.com/admin/v2/projects/" + projectId + "/config";
using var stream = new FileStream(serviceAccountJsonFile, FileMode.Open, FileAccess.Read);
var accessToken = GoogleCredential
.FromStream(stream) // Loads key file
.CreateScoped(scopes) // Gathers scopes requested
.UnderlyingCredential // Gets the credentials
.GetAccessTokenForRequestAsync().Result; // Gets the Access Token
var body = JsonConvert.SerializeObject(authorizedDomains);
using (var client = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Patch, url) {
Content = new StringContent(body,System.Text.Encoding.UTF8)
};
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer " + accessToken);
try
{
var response = client.SendAsync(request).Result;
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
}
catch (HttpRequestException ex)
{
// Failed
}
}
Thanks #Jean Costa and #Yan Naing
here is my php implemetation
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\TransferException;
use Google\Service\IdentityToolkit;
use Google\Service\IAMCredentials;
$KEY_FILE_LOCATION = storage_path('/app/credentials/service-account-1.json') ;
if (!file_exists($KEY_FILE_LOCATION)) {
throw new Exception(sprintf('file "%s" does not exist', $KEY_FILE_LOCATION));
}
$json= file_get_contents($KEY_FILE_LOCATION);
if (!$config = json_decode($json, true)) {
throw new Exception('invalid json for auth config');
}
$client = new \Google\Client();
$client->setAuthConfig($config );
$client->setScopes([ "https://www.googleapis.com/auth/identitytoolkit",
"https://www.googleapis.com/auth/firebase",
"https://www.googleapis.com/auth/cloud-platform"]);
$service = new IdentityToolkit($client);
// Get the Identity Toolkit API client
$idToolkit = $service->relyingparty;
//Get current config
$current_config= $idToolkit->getProjectConfig();
//Get service account access token
$access_token_req = new IAMCredentials\GenerateAccessTokenRequest();
$access_token_req->setScope( "https://www.googleapis.com/auth/firebase");
$credentials = new IAMCredentials($client);
$access_token = $credentials->projects_serviceAccounts->generateAccessToken("projects/-/serviceAccounts/{$config["client_email"]}" , $access_token_req )->getAccessToken();
// Generate the request URL (https://cloud.google.com/identity-platform/docs/reference/rest/v2/projects/updateConfig)
$idToolkitConfigUrl = "https://identitytoolkit.googleapis.com/admin/v2/projects/{$config["project_id"]}/config";
$authorized_domains = [ 'authorizedDomains' => array_merge( ['twomore.com'],$current_config->authorizedDomains)];
$client = new GuzzleClient( );
$response = null;
try {
$response = $client->request('PATCH', $idToolkitConfigUrl, [
'verify' => Helpers::isProduction() ? true : false ,
'http_errors'=> false, //off 4xx and 5xx exceptioins
'json' => $authorized_domains ,
'headers' => [
"Authorization" => "Bearer " . $access_token ,
"Accept" => "application/json",
]
]);
} catch (TransferException $e) {
throw new Exception( $e->getMessage());
}
$data = json_decode($response->getBody()->getContents(),true);
if($response->getStatusCode()!==200){
throw new Exception($response->getReasonPhrase() . ( isset($data['exception']['message']) ? " - " . $data['exception']['message'] : ""));
}
return response()->json(['data' => [
'authorized_domains' => $data['authorizedDomains']
]]);

App Maker: Sending Email, client script to server script function not working. Failed due to illegal value in property: a

I am new to AppMaker but I have developer experience.
The application is a Project Tracker Application
What I expect to happen: When creating a project the user uses a User Picker to select the users associated with that project. When the project is created I want to email the users associated with that project.
The issue: On clicking the Add button addProject(addButton) client script function is called.
Inside this function sendEmailToAssignees(project, assignees) is called which should reach out to the Server script and run the notifyAboutProjectCreated(project, assignees) but that is not happening.
Things to know: With logging I never reach 'Trying to send email' so I seem to never reach my server script. Also, On client script when I comment out sendEmailToAssignees function everything runs smooth. I have looked at this documentation as a resource so I feel my implementation is okay. https://developers.google.com/appmaker/scripting/client#client_script_examples
The final error message I get is:
Failed due to illegal value in property: a at addProject
(AddProject:110:24) at
AddProject.Container.PanelAddProject.Form1.Spring.ButtonAdd.onClick:1:1
Am I missing something here? Any help would be greatly appreciated. Thank you!
Client Script
function sendEmailToAssignees(project, assignees) {
google.script.run
.withSuccessHandler(function() {
console.log('Sending Email Success');
}).withFailureHandler(function(err) {
console.log('Error Sending Email: ' + JSON.stringify(err));
})
.notifyAboutProjectCreated(project, assignees);
}
function addProject(addButton) {
if (!addButton.root.validate()) {
return;
}
addButton.datasource.createItem(function(record) {
var page = app.pages.AddProject;
var pageWidgets = page.descendants;
var trainees = pageWidgets.AssigneesGrid.datasource.items;
var traineesEmails = trainees.map(function(trainee) {
return trainee.PrimaryEmail;
});
record.Assignee = traineesEmails.toString();
var assignees = traineesEmails.toString();
var project = record;
updateAllProjects(record);
console.log('update all projects done');
sendEmailToAssignees(project, assignees);
console.log('Send Email done');
if (app.currentPage !== app.pages.ViewProject) {
return;
}
gotoViewProjectPageByKey(record._key, true);
});
gotoViewProjectPageByParams();
}
Server Script
function notifyAboutProjectCreated(project, assignees) {
console.log('Trying to send email');
if (!project) {
return;
}
var settings = getAppSettingsRecord_()[0];
if (!settings.EnableEmailNotifications) {
return;
}
var data = {
appUrl: settings.AppUrl,
assignee: project.Assignee,
owner: project.Owner,
startDate: project.StartDate,
endDate: project.EndDate,
jobType: project.Type,
jobId: project.Id
};
// Email Subject
var subjectTemplate = HtmlService.createTemplate(settings.NotificationEmailSubjectJob);
subjectTemplate.data = data;
var subject = subjectTemplate.evaluate().getContent();
// Email Body
var emailTemplate =
HtmlService.createTemplate(settings.NotificationEmailBodyJob);
emailTemplate.data = data;
var htmlBody = emailTemplate.evaluate().getContent();
console.log('About to send email to:', assignees);
sendEmail_(null, assignees, subject, htmlBody);
}
The reason you are getting this error is because you are trying to pass the client "project record" to the server. If you need to access the project, then pass the record key to the server and then access the record on the server using the key.
CLIENT:
function sendEmailToAssignees(project, assignees) {
var projectKey = project._key;
google.script.run
.withSuccessHandler(function() {
console.log('Sending Email Success');
}).withFailureHandler(function(err) {
console.log('Error Sending Email: ' + JSON.stringify(err));
})
.notifyAboutProjectCreated(projectKey , assignees);
}
SERVER:
function notifyAboutProjectCreated(projectKey, assignees) {
console.log('Trying to send email');
var project = app.models.<PROJECTSMODEL>.getRecord(projectKey);
if (!project) {
return;
}
//Rest of the logic
}
The project record object in the client is not the same as the project record object in the server; hence the ilegal property value error.

Meteor.js and Custom OpenId Connect server

How to do authentication via custom token server in Meteor.js?
Is there any package like accounts-google for custom token server which handles authentication by just taking token endpoints, client id, secrete, and scope as configuration parameter.
I don't know of a generic oauth package. But it shouldn't be too difficult to write a package for your particular server, as there are a number of examples to look at.
Using accounts-github as an example, here's the code for making the connection on the client. Note the endpoint URL, client id, scope, etc. This will handle the popup for you, but you'll probably want to include custom CSS:
var loginUrl =
'https://github.com/login/oauth/authorize' +
'?client_id=' + config.clientId +
'&scope=' + flatScope +
'&redirect_uri=' + OAuth._redirectUri('github', config) +
'&state=' + OAuth._stateParam(loginStyle, credentialToken);
OAuth.launchLogin({
loginService: "github",
loginStyle: loginStyle,
loginUrl: loginUrl,
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
credentialToken: credentialToken,
popupOptions: {width: 900, height: 450}
});
And here's a snippet from the server side, completing the process to get an access token:
var getAccessToken = function (query) {
var config = ServiceConfiguration.configurations.findOne({service: 'github'});
if (!config)
throw new ServiceConfiguration.ConfigError();
var response;
try {
response = HTTP.post(
"https://github.com/login/oauth/access_token", {
headers: {
Accept: 'application/json',
"User-Agent": userAgent
},
params: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('github', config),
state: query.state
}
});
} catch (err) {
throw _.extend(new Error("Failed to complete OAuth handshake with Github. " + err.message),
{response: err.response});
}
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error("Failed to complete OAuth handshake with GitHub. " + response.data.error);
} else {
return response.data.access_token;
}
};
And utilizing the token to get the user identity:
var getIdentity = function (accessToken) {
try {
return HTTP.get(
"https://api.github.com/user", {
headers: {"User-Agent": userAgent}, // http://developer.github.com/v3/#user-agent-required
params: {access_token: accessToken}
}).data;
} catch (err) {
throw _.extend(new Error("Failed to fetch identity from Github. " + err.message),
{response: err.response});
}
};
The github and the accounts-github packages should be very helpful as references.

Basic HTTP authentication in Node.JS?

I'm trying to write a REST-API server with NodeJS like the one used by Joyent, and everything is ok except I can't verify a normal user's authentication. If I jump to a terminal and do curl -u username:password localhost:8000 -X GET, I can't get the values username:password on the NodeJS http server. If my NodeJS http server is something like
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, "127.0.0.1");
, shouldn't I get the values username:password somewhere in the req object that comes from the callback ?
How can I get those values without having to use Connect's basic http auth ?
The username:password is contained in the Authorization header as a base64-encoded string.
Try this:
const http = require('http');
http.createServer(function (req, res) {
var header = req.headers.authorization || ''; // get the auth header
var token = header.split(/\s+/).pop() || ''; // and the encoded auth token
var auth = Buffer.from(token, 'base64').toString(); // convert from base64
var parts = auth.split(/:/); // split on colon
var username = parts.shift(); // username is first
var password = parts.join(':'); // everything else is the password
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('username is "' + username + '" and password is "' + password + '"');
}).listen(1337, '127.0.0.1');
From HTTP Authentication: Basic and Digest Access Authentication - Part 2 Basic Authentication Scheme (Pages 4-5)
Basic Authentication in Backus-Naur Form
basic-credentials = base64-user-pass
base64-user-pass = <base64 [4] encoding of user-pass,
except not limited to 76 char/line>
user-pass = userid ":" password
userid = *<TEXT excluding ":">
password = *TEXT
If you're using express, you can use the connect plugin (included with express):
//Load express
var express = require('express');
//User validation
var auth = express.basicAuth(function(user, pass) {
return (user == "super" && pass == "secret");
},'Super duper secret area');
//Password protected area
app.get('/admin', auth, routes.admin);
You can use node-http-digest for basic auth or everyauth, if adding authorization from external services are in you roadmap.
I use this code for my own starter sites with auth.
It does several things:
basic auth
return index.html for / route
serve content without crashing and silent handle the error
allow port parameter when running
minimal amount of logging
Before using the code, npm install express
var express = require("express");
var app = express();
//User validation
var auth = express.basicAuth(function(user, pass) {
return (user == "username" && pass == "password") ? true : false;
},'dev area');
/* serves main page */
app.get("/", auth, function(req, res) {
try{
res.sendfile('index.html')
}catch(e){}
});
/* add your other paths here */
/* serves all the static files */
app.get(/^(.+)$/, auth, function(req, res){
try{
console.log('static file request : ' + req.params);
res.sendfile( __dirname + req.params[0]);
}catch(e){}
});
var port = process.env.PORT || 8080;
app.listen(port, function() {
console.log("Listening on " + port);
});
It can be implemented easily in pure node.js with no dependency, this is my version which is based on this answer for express.js but simplified so you can see the basic idea easily:
const http = require('http');
http.createServer(function (req, res) {
const userpass = Buffer.from(
(req.headers.authorization || '').split(' ')[1] || '',
'base64'
).toString();
if (userpass !== 'username:password') {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="nope"' });
res.end('HTTP Error 401 Unauthorized: Access is denied');
return;
}
res.end('You are in! Yay!!');
}).listen(1337, '127.0.0.1');
The restify framework (http://mcavage.github.com/node-restify/) includes an authorization header parser for "basic" and "signature" authentication schemes.
You can use http-auth module
// Authentication module.
var auth = require('http-auth');
var basic = auth.basic({
realm: "Simon Area.",
file: __dirname + "/../data/users.htpasswd" // gevorg:gpass, Sarah:testpass ...
});
// Creating new HTTP server.
http.createServer(basic, function(req, res) {
res.end("Welcome to private area - " + req.user + "!");
}).listen(1337);

Resources