Octokit - how to authenticate as an app (JWT) - authentication

So I'm building a github app, and I am wanting to be able to authenticate as the app so I can do graphql calls as that user. So, I can authenticate as the app, and get the JWT, but I can't seem to use the JWT. Code looks like:
const { Octokit } = require("#octokit/core");
const { createAppAuth} = require("#octokit/auth-app");
const fs = require('fs')
const auth = createAppAuth( {
appId: process.env.APP_ID,
privateKey: fs.readFileSync(__dirname + "/../" + process.env.PRIVATE_KEY_PATH, "utf-8"),
clientId: process.env.CLIENT_ID,
clientSecret: process.env.WEBHOOK_SECRET
})
// Send requests as GitHub App
async function main() {
const {token} = await auth({type: "app"})
console.log(token);
const appOctokit = new Octokit({
baseUrl: 'https://github.<company>.com/api/v3',
auth: `token ${token}`
});
const { slug } = await appOctokit.request("GET /user");
console.log("authenticated as %s", slug);
}
main().then().catch(err => {
console.log(err.message)
console.log(err.stack)
console.log("oops")
})
I end up getting an HttpError: Bad Credentials.
What am I missing?

The reason for the bad credentials error though is that you are trying to authenticate as the app for the GET /user request. This is a user-specific request, which requires an OAuth token.
Try sending GET /app instead, it should work.
If you do want to authenticate as a user, then there are two ways to receive an OAuth token through a GitHub App (GitHub calls these user-to-server token, because the token is authorized by both, the app and the user).
OAuth Web flow
OAuth Device flow
For the Web Flow, see https://github.com/octokit/auth-app.js/#user-authentication-web-flow. You will need a server that can receive the http redirect from GitHub. You can use the #octokit/app SDK which exports a node middleware for that and other OAuth related usecases , as well as webhooks: https://github.com/octokit/app.js/#middlewares
For the OAuth Device Flow, see https://github.com/octokit/auth-app.js/#user-authentication-device-flow.
If you want to authenticate using the OAuth Device Flow without exposing the OAuth Client Secret, you can use the dedicated OAuth Device Flow authentication strategy: https://github.com/octokit/auth-oauth-device.js

Related

NextJS/Next-Auth Backend Authentication with OAuth

I am currently building a web app based on a turborepo (monorepo) in which I want to use Discord OAuth login with next-auth. Therefore I have two modules web and api, where api is my express backend with discord.js. The web app is basically a dashboard for a Discord bot.
I figured that next-auth only provides client side authentication. So my question is how can I validate the OAuth session from the client side in the best manner?
My middleware for express currently looks like this:
function throwUnauthorized(res: Response) {
res.status(401).json({ code: 401, message: 'Unauthorized' });
}
export async function isAuthorized(req: Request, res: Response, next: NextFunction) {
try {
const authorization = req.headers.authorization;
if (!authorization) {
return throwUnauthorized(res);
}
// validate token with Discord API
const { data } = await axios.get('https://discord.com/api/oauth2/#me', {
headers: { Authorization: authorization },
});
// protect against token reuse
if (!data || data.application.id !== process.env.TC_DISCORD_CLIENT_ID) {
return throwUnauthorized(res);
}
// map to database user
let user = await User.findOne({ id: data.user.id });
user ??= await User.create({ id: data.user.id });
data.user.permissions = user.permissions;
req.user = data.user;
next();
} catch (error) {
return throwUnauthorized(res);
}
}
In this approach the Discord OAuth Token would be send via the Authorization header and checked before each request that requires Authorization. Which leads to my problem: The token needs to be validated again causing multiple request to Discord API.
Is there a better way to handle this? Because I need to map Discord user profiles to database profiles. I read that you could try decode the jwt session token from next-auth, but this did not work when I tested it.
Maybe there is a whole different project structure suggested for my project. But I thought I should separate the api and web-app since I would have needed a custom express server because it includes the Discord bot and Prometheus logging functions. I am open for suggestions and your thoughts!

Is there a way to call the google books api from firebase auth to get a logged in users bookshelves in "My Library"?

I have the firebase auth set up and can log in, get the consent screen to access google books account, get the token, and log out.
Google will be deprecating the gapi.auth2 module and using Google Identity Services so I'm trying to find a solution that doesn't involve using the gapi.auth2 module.
The following website has an example of how to use the Google Identity Services:
https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gis-only
In this example they use the Google Identity Services library to get the access_token to then pass it along in the The XMLHttpRequest object to request data from a logged in users account. Seeing as I can already get the access_token I'm trying to make the request without using the Google Identity Service or Google API Client Library for JavaScript and just use a XMLHttpRequest.
The below code returns a response of the index.html page and not the response from the google books API.
function getData(access_token){
if(access_token !== undefined){
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// Typical action to be performed when the document is ready:
console.log(xhttp.responseText);
}
};
xhttp.open("GET", "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)");
xhttp.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhttp.send();
}
}
function Login(){
useEffect(()=>{
signInWithGoogle();
});
provider.addScope("https://www.googleapis.com/auth/books");
const signInWithGoogle = () =>{
signInWithPopup(auth, provider).then((result)=>{
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
if(result.user){
getData(token);
}
}).catch((error)=>{
if(error.code === 'auth/popup-closed-by-user'){
}
});
}
return (
<p>Logging in...</p>
)
}
export default Login;
I know that "books/v1/mylibrary/bookshelves/4/volumes?fields=totalItems, items(id)" works because I've used it in a working version that doesn't use firebase as it's just a plain html version that uses the Google Services Identity library with the Google API Client Library for JavaScript. The network request look the same and have the same access_token with my adapted version and the plain html version that works.
Is there a way to call the google books API from firebase auth to get a logged in users bookshelves in "My Library"?

Authenticate External App to Salesforce instance

I am building a mobile application that will need to fetch data from my salesforce instance. I have no problem with SOQL to grab the appropriate data. However, I do not want the user of the mobile app to have to log in to get a token, which I need to use to access the data.
Is it possible to authenticate the app via appId and client secret in the application to allow the appropriate use/access of the APIs? This would be similar to authenticating an app to a Parse or Firebase instance without requiring an authenticated user.
Thanks!
This is an example in nodejs/Express , in this example i get the accounts:
var express = require('express');
var router = express.Router();
var nforce = require('nforce');
/* GET home page. */
router.get('/', function(req, res, next) {
var accounts =[];
// create the connection with the Salesforce connected app
var org = nforce.createConnection({
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
redirectUri: process.env.CALLBACK_URL,
mode: 'single'
});
// authenticate and return OAuth token
org.authenticate({
username: process.env.USERNAME,
password: process.env.PASSWORD+process.env.SECURITY_TOKEN
}, function(err, resp){
if (!err) {
console.log('Successfully logged in! Cached Token: ' +
org.oauth.access_token);
// execute the query
org.query({ query: 'select id, name from account' }, function(err, resp){
if(!err && resp.records) {
// output the account names
for (i=0; i<resp.records.length;i++) {
//console.log(resp.records[i].get('name'));
accounts.push(resp.records[i].get('name'));
}
res.render('index', { title:'Accounts',accounts:accounts });
}
});
}
if (err) console.log(err);
});
//console.log(accounts);
});
module.exports = router;
You need to get you api crendentials for authenticate , it does not matter what language are you using the concept is the same.
USERNAME : Your Salesforce User name
PASSWORD: Your Salesforce Password
SECURITY_TOKEN: Your user security token , if you don't have it you can go to My Settings -Personal -Reset my security token and Salesforce will send you the token to your email.
The other parameters are parameters you get from your app , so you have to register the app to get the security tokens.
For create a connected apd You go to : Setup -Build-Create Apps in the section of Connected Apps create a new one. the connected api generate a consumer key and a Consumer secret.
CLIENT_ID: Is the consumer key
CLIENT_SECRET:Is the Consumer secret
CALLBACK_URL: The callback url in my example is : http://localhost:3000
This is not natively possible. All access to data/meta data in salesforce goes through a salesforce user account.
User accounts auth via username/pass, or via SSO (oAuth/SAML), or "Delegated Auth" (a pre-SAML auth service).
Creating a "Connected App" is a feature, enabled by the salesforce admin, which enables your mobile app to connect via oAuth, along with a public/private key pair.
However, Login is still required.
Perhaps you could place middleware between salesforce and your API - the middleware would connect to salesforce using a salesforce user account, while the API that it exposes accepts your public/private key.

How to pass Firebase Auth Token from client to server?

The website that I'm working on uses Firebase authentication and different users that login have different permissions as to which pages they can visit.
The way signing in is setup is similar to this post:
User Logins in with two parameters - "id" and "email"
Server uses these to create a custom "uid", then uses the Firebase Admin SDK to create a custom token that is sent back to the client.
The client logs in with the Javascript Firebase SDK - firebase.auth().signInWithCustomToken()
Now that the user is logged in, they can click different pages - i.e. '/foo', '/bar'
The issue I'm running into is that when they visit new pages, I'm trying to pass the token from the client back to the server (almost identical to how its done in this Firebase Doc ), verify the token & check if it has permission to view the webpage.
I'm trying to figure out the best (& most secure) way to do this. I've considered the following option:
Construct a URL with the token, but I've heard this isn't good practice because the token is getting exposed and session hijacking becomes a lot easier.
I've been trying to pass the token in the request header, but from my understanding you can't add headers when the user clicks on a link to a different page (or if its redirected in javascript). The same issue applies to using POST.
What can I do to securely pass this information to the server and check permissions when a user clicks on a link to a different page?
You can get the accessToken (idToken) on client side by:
var accessToken = null;
firebase.auth().currentUser
.getIdToken()
.then(function (token) {
accessToken = token;
});
and pass it in your request headers:
request.headers['Authorization'] = 'Bearer ' + accessToken;
and on your server side get the token with your prefered method and authenticate the request with Firebase Admin SDK, like (Node.js):
firebaseAdmin.auth()
.verifyIdToken(accessToken)
.then(decodedIdToken => {
return firebaseAdmin.auth().getUser(decodedIdToken.uid);
})
.then(user => {
// Do whatever you want with the user.
});
Nowadays, it looks like we're meant to use httpsCallable() client-side to get an object pre-authorized to talk to your endpoint.
eg:
// # ./functions/index.js
exports.yourFunc = functions.https.onCall((data, context) => {
// Checking that the user is authenticated.
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
// ... rest of your method
});
// ./src/models/addMessage.js
const firebase = require("firebase");
require("firebase/functions");
firebase.initializeApp({
apiKey: '### FIREBASE API KEY ###',
authDomain: '### FIREBASE AUTH DOMAIN ###',
projectId: '### CLOUD FUNCTIONS PROJECT ID ###'
databaseURL: 'https://### YOUR DATABASE NAME ###.firebaseio.com',
});
var functions = firebase.functions();
// This is the new code:
var yourFunc = firebase.functions().httpsCallable('yourFunc');
yourFunc({foo: bar}).then(function(result) {
// ...
});
From firebase documentation

Adal.js does not get tokens for external api endpoint resource

I'm trying out adal.js with an Angular SPA (Single Page Application) web site that gets data from an external Web API site (different domain). Authentication against the SPA was easy with adal.js, but getting it to communicate with the API is not working at all when bearer tokens are required. I have used https://github.com/AzureAD/azure-activedirectory-library-for-js as template in addition to countless blogs.
The problem is that when I set up endpoints while initiating adal.js, adal.js seems to redirect all outgoing endpoint traffic to microsofts login service.
Observations:
Adal.js session storage contains two adal.access.token.key entries. One for the client ID of the SPA Azure AD application and one for the external api. Only the SPA token has a value.
If I do not inject $httpProvider into adal.js, then calls go out to the external API and I get a 401 in return.
If I manually add the SPA token to the http header ( authorization: bearer 'token value') I get a 401 in return.
My theory is that adal.js is unable to retrieve tokens for endpoints (probably because I configured something wrong in the SPA) and it stops traffic to the endpoint since it is unable to get a required token. The SPA token cannot be used against the API since it does not contain the required rights. Why is adal.js not getting tokens for endpoints and how can I fix it?
Additional information:
The client Azure AD application is configured to use delegated permissions against the API and oauth2AllowImplicitFlow = true in app manifest.
The API Azure AD application is configured for impersonation and oauth2AllowImplicitFlow = true (do not think that is required, but tried it). It is multi tenant.
The API is configured to allow all CORS origins and it works correctly when used by another web app using impersonation (hybrid MVC (Adal.net) + Angular).
Session storage:
key (for the SPA application): adal.access.token.keyxxxxx-b7ab-4d1c-8cc8-xxx value: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1u...
key (for API application): adal.access.token.keyxxxxx-bae6-4760-b434-xxx
value:
app.js (Angular and adal configuration file)
(function () {
'use strict';
var app = angular.module('app', [
// Angular modules
'ngRoute',
// Custom modules
// 3rd Party Modules
'AdalAngular'
]);
app.config(['$routeProvider', '$locationProvider',
function ($routeProvider, $locationProvider) {
$routeProvider
// route for the home page
.when('/home', {
templateUrl: 'App/Features/Test1/home.html',
controller: 'home'
})
// route for the about page
.when('/about', {
templateUrl: 'App/Features/Test2/about.html',
controller: 'about',
requireADLogin: true
})
.otherwise({
redirectTo: '/home'
})
//$locationProvider.html5Mode(true).hashPrefix('!');
}]);
app.config(['$httpProvider', 'adalAuthenticationServiceProvider',
function ($httpProvider, adalAuthenticationServiceProvider) {
// endpoint to resource mapping(optional)
var endpoints = {
"https://localhost/Api/": "xxx-bae6-4760-b434-xxx",
};
adalAuthenticationServiceProvider.init(
{
// Config to specify endpoints and similar for your app
clientId: "xxx-b7ab-4d1c-8cc8-xxx", // Required
//localLoginUrl: "/login", // optional
//redirectUri : "your site", optional
extraQueryParameter: 'domain_hint=mydomain.com',
endpoints: endpoints // If you need to send CORS api requests.
},
$httpProvider // pass http provider to inject request interceptor to attach tokens
);
}]);
})();
Angular code for calling endpoint:
$scope.getItems = function () {
$http.get("https://localhost/Api/Items")
.then(function (response) {
$scope.items = response.Items;
});
Ok, I've been bashing my head against the wall to figure this out. Trying to make my ADAL.js SPA app (sans angular) successfully make cross-domain XHR requests over to my precious CORS-enabled Web API.
This sample app, the one all the newbies like me are using, has this problem: it features an API and SPA all served from the same domain - and only requires a single AD Tenant app registration. This only confuses things when it comes time to pull things apart into separate pieces.
So, out of the box, the sample has this Startup.Auth.cs which works OK, as far as the sample goes...
public void ConfigureAuth(IAppBuilder app) {
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
});
}
but, you need to modify the above code, drop the Audience assignment, and go for an array of audiences.. That's right: ValidAudiences .. So, for every SPA client that is talking to your WebAPI, you'll want to put the ClientID of your SPA registration in this array...
It should look like this...
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new [] {
ConfigurationManager.AppSettings["ida:Audience"],//my swagger SPA needs this 1st one
"b2d89382-f4d9-42b6-978b-fabbc8890276",//SPA ClientID 1
"e5f9a1d8-0b4b-419c-b7d4-fc5df096d721" //SPA ClientID 2
},
RoleClaimType = "roles" //Req'd only if you're doing RBAC
//i.e. web api manifest has "appRoles"
}
});
}
EDIT
Ok, based on #JonathanRupp's feedback, I was able to reverse out the Web API solution I was using shown above, and was able to modify my client JavaScript as shown below to make everything work.
// Acquire Token for Backend
authContext.acquireToken("https://mycorp.net/WebApi.MyCorp.RsrcID_01", function (error, token) {
// Handle ADAL Error
if (error || !token) {
printErrorMessage('ADAL Error Occurred: ' + error);
return;
}
// Get TodoList Data
$.ajax({
type: "GET",
crossDomain: true,
headers: {
'Authorization': 'Bearer ' + token
},
url: "https://api.mycorp.net/odata/ToDoItems",
}).done(function (data) {
// For Each Todo Item Returned, do something
var output = data.value.reduce(function (rows, todoItem, index, todos) {
//omitted
}, '');
// Update the UI
//omitted
}).fail(function () {
//do something with error
}).always(function () {
//final UI cleanup
});
});
ADAL.js does get the access_token apart from id_token for calling Azure AD protected API running on different domain.
Initially, during login, it only takes id_token. This token has the access for accessing resource of the same domain.
But, on calling the API running in different domain, adal interceptor checks if the API URL is configured in as endpoint in adal.init().
It is only then that the access token is called for the requested resource. It also necessitates that the SPA is configured in the AAD to access API APP.
The key to achieve this is following:
1. Add endpoints in the adal.init()
var endpoints = {
// Map the location of a request to an API to a the identifier of the associated resource
//"Enter the root location of your API app here, e.g. https://contosotogo.azurewebsites.net/":
// "Enter the App ID URI of your API app here, e.g. https://contoso.onmicrosoft.com/TestAPI",
"https://api.powerbi.com": "https://analysis.windows.net/powerbi/api",
"https://localhost:44300/": "https://testpowerbirm.onmicrosoft.com/PowerBICustomServiceAPIApp"
};
adalProvider.init(
{
instance: 'https://login.microsoftonline.com/',
tenant: 'common',
clientId: '2313d50b-7ce9-4c0e-a142-ce751a295175',
extraQueryParameter: 'nux=1',
endpoints: endpoints,
requireADLogin: true,
//cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
// Also, token acquisition for the To Go API will fail in IE when running on localhost, due to IE security restrictions.
},
$httpProvider
);
Give permission to the SPA application in Azure AD to access the API application:
You may refer this link for details : ADAL.js deep dive
You need to make your Web API aware of your Client application. It's not enough to add delegated permission to API from your Client.
To make the API client aware, go to Azure management portal, download API's manifest and add ClientID of your Client application to the list of "knownClientApplications".
To allow Implicit flow you need to set "oauth2AllowImplicitFlow" to true in the manifest as well.
Upload the manifest back to API application.
I'm not sure if our setup is exactly the same, but I think it it comparable.
I have a Angular SPA that uses and external Web API through Azure API Management (APIM). My code might not be best practice, but it works for me so far :)
The SPAs Azure AD app has a delegated permission to access the External APIs Azure AD app.
The SPA (is based upon the Adal TodoList SPA sample)
app.js
adalProvider.init(
{
instance: 'https://login.microsoftonline.com/',
tenant: 'mysecrettenant.onmicrosoft.com',
clientId: '********-****-****-****-**********',//ClientId of the Azure AD app for my SPA app
extraQueryParameter: 'nux=1',
cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
},
$httpProvider
);
Snippet from the todoListSvc.js
getWhoAmIBackend: function () {
return $http.get('/api/Employee/GetWhoAmIBackend');
},
Snippets from the EmployeeController
public string GetWhoAmIBackend()
{
try
{
AuthenticationResult result = GetAuthenticated();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var request = new HttpRequestMessage()
{
RequestUri = new Uri(string.Format("{0}", "https://api.mydomain.com/secretapi/api/Employees/GetWhoAmI")),
Method = HttpMethod.Get, //This is the URL to my APIM endpoint, but you should be able to use a direct link to your external API
};
request.Headers.Add("Ocp-Apim-Trace", "true"); //Not needed if you don't use APIM
request.Headers.Add("Ocp-Apim-Subscription-Key", "******mysecret subscriptionkey****"); //Not needed if you don't use APIM
var response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
var res = response.Content.ReadAsStringAsync().Result;
return res;
}
return "No dice :(";
}
catch (Exception e)
{
if (e.InnerException != null)
throw e.InnerException;
throw e;
}
}
private static AuthenticationResult GetAuthenticated()
{
BootstrapContext bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as BootstrapContext;
var token = bootstrapContext.Token;
Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext =
new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/mysecrettenant.onmicrosoft.com");
//The Client here is the SPA in Azure AD. The first param is the ClientId and the second is a key created in the Azure Portal for the AD App
ClientCredential credential = new ClientCredential("clientid****-****", "secretkey ********-****");
//Get username from Claims
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
//Creating UserAssertion used for the "On-Behalf-Of" flow
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
//Getting the token to talk to the external API
var result = authContext.AcquireToken("https://mysecrettenant.onmicrosoft.com/backendAPI", credential, userAssertion);
return result;
}
Now, in my backend external API, my Startup.Auth.cs looks like this:
The external API
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
SaveSigninToken = true
},
AuthenticationType = "OAuth2Bearer"
});
}
Please let me know if this helps or if I can be of further assistance.