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!
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"?
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
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.
I am trying to design the authentication flow of an Ember application with a Rails backend. I basically want to authenticate users via Google/Facebook/etc., I do not want to provide an 'independent' authentication service. I do want to maintain a list of users of course on the server side, potentially merging different authentications from different sources into the same user. I will not interact on behalf of the user on Google/Facebook from the client side, but I will do that on the server side.
For the above reason I was planning to do the following:
I will use torii to fetch an auth_token on the client side and I will pass that onto the server side, where I will validate it, convert it into an access token.
I will generate a custom token on the server side which I will send back to the client and require all further API calls to be accompanied by that token. I will not share the access token with the client at all.
Would you say that this is an optimal flow?
In terms of implementation, I have been able to get auth_tokens from the different providers using the example here. I am completely unsure however:
if I need ember-simple-auth or only torii (how do these two complement each other?)
how do I pass the auth token to the server side? With the code below I can get the auth token, but is this the proper place to implement the call to the API?
export default Ember.Route.extend({
actions: {
googleLogin: function() {
var _this = this;
this.get('session').authenticate('simple-auth-authenticator:torii', 'google-oauth2').then(
function() {console.log(_this.get('session.secure.authorizationCode'));}
);
return;
},
facebookLogin: function() {
this.get('session').authenticate('simple-auth-authenticator:torii', 'facebook-oauth2');
return;
}
}
});
how do I make all further requests to the API to be accompanied by a specific token?
should I use devise on the server side to make it easier or not?
I have been implemented exactly the same kind of workflow.
I used ember-simple-auth with ember-simple-auth-torii and implemented a custom authenticator to achieve this goal.
Ember-simple-auth provides an example of a custom authenticator here .
Your custom authenticator implementation will look like the following
First get auth_token using torii
Then valid this auth_token against your backend in order to get your custom token
Your authenticate callback in your custom authenticator will basically look like the following :
authenticate: function(provider, options) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
self.torii.open(provider, options || {}).then(function(data) {
var endpoint = '/token'; // Your API endpoint used to get your cutom token
var dataToSend = { // Data sent to your endpoint
grant_type: 'authorization_code',
code: data.accessToken,
access_token: data.accessToken
};
$.post(endpoint, dataToSend).done(function(response) {
response.provider = provider;
resolve(data);
}).fail(function(response) {
response.provider = provider;
reject(data);
})
}, reject)
})
}
Once you have the custom authenticator initilized you can use it this way on your controllers :
this.get('session').authenticate(
'authenticator:customauthenticator', // Or wathever name you gave
'facebook-connect' // Any compatible torii provider
).then(function(user) {
console.log(user); // Will display ajax response from your endpoint
})
Finally, if you want your custom token to be automatically sent with all ajax request, you can use the ember-simple-auth oauth2-bearer authorizer.