microsoftarchive/botauth

botAuth and Auth0 and Invalid authorization code

jtsampson opened this issue · 1 comments

Hello,

I'm trying to use botauth to connect to an Auth0 client using the passport-auth0 strategy to authorize access to my api that sits behind an API gateway.

Using the Bot Framework Emulator, and commenting out lines 36-38 of index.js so I can use a an http callback address (perhaps that is my issue), I am able to enter my credentials with Auth0 , click login and the bot logs my profile

However.... after authenticating in the browser, the bot redirects to http://WEB_HOSTNAME:PORT/botauth/auth0?state=... hangs there for a bit, then redirects to http://WEB_HOSTNAME:PORT/botauth/auth0/callback?code=... with the response:

{"message":"Invalid authorization code"}

and I never get my bot conversation resumed in the emulator.

I wrote up a quick test based on the project's pintrest example it it helps anyone work out what I am doing wrong...any help would be appreciated...

Notes:

  • Same behavior whether or not I use server.use(clientSessions({ secret: BOTAUTH_SECRET }));
  • Tested against Auth0 Client of type Regular Web Application
    ** Token Endpoint Authentication Method : POST
    ** Allowed Callback URLs : configured with proper callback URL (http) in this case
    ** Allowed Logout URLs : hadn't got that far
  • Tested against Auth0 Client of type Non Interactive Client
    ** Token Endpoint Authentication Method : POST
    ** Allowed Callback URLs : configured with proper callback URL (http) in this case
    ** Allowed Logout URLs : hadn't got that far

Thanks in advance.

"use strict";

const restify = require("restify");
const builder = require("botbuilder");
const envx = require("envx");
const botauth = require("botauth");
const clientSessions = require("client-sessions");

const passport = require("passport");
const Auth0Strategy = require('passport-auth0')

//contextual service information
const WEBSITE_HOSTNAME = envx("WEB_HOSTNAME");
const PORT = envx("PORT", 3998);

//bot application identity
const MICROSOFT_APP_ID = envx("MICROSOFT_APP_ID");
const MICROSOFT_APP_PASSWORD = envx("MICROSOFT_APP_PASSWORD");

//oauth details for pinterest
const AUTH0_DOMAIN= envx("AUTH0_DOMAIN");
const AUTH0_CLIENT_ID = envx("AUTH0_CLIENT_ID");
const AUTH0_CLIENT_SECRET = envx("AUTH0_CLIENT_SECRET");

//encryption key for saved state
const BOTAUTH_SECRET = envx("BOTAUTH_SECRET");

var server = restify.createServer();
server.use(restify.bodyParser());
server.use(restify.queryParser());
server.use(clientSessions({ secret: BOTAUTH_SECRET }));

// Create chat connector with bot's Microsoft app identity
var connector = new builder.ChatConnector({
    appId: MICROSOFT_APP_ID,
    appPassword: MICROSOFT_APP_PASSWORD
});

// Create bot builder client and connect it to restify server
var bot = new builder.UniversalBot(connector);
server.post('/api/messages', connector.listen());

// Initialize with the strategies we want to use
var ba = new botauth.BotAuthenticator(server, bot, { baseUrl : "http://" + WEBSITE_HOSTNAME, secret : BOTAUTH_SECRET })
    .provider("auth0", (options) => {
        return new Auth0Strategy({
            domain: AUTH0_DOMAIN,
            clientID: AUTH0_CLIENT_ID,
            clientSecret: AUTH0_CLIENT_SECRET,
            scope: 'openid profile email identities',
            //scope: ['openid', 'profile', 'email', 'identities'],
            //scope: [ 'read_public', 'read_relationships' ],
            callbackURL: options.callbackURL
            //callbackURL: options.callbackURL
        }, (accessToken, refreshToken, profile, done) => {
            profile = profile || {};
            profile.accessToken = accessToken;
            profile.refreshToken = refreshToken;
            profile.provider = "auth0"; // workaround
            console.log('===========')
            console.log(profile)
            console.log('===========')
            return done(null, profile);
        });
    });



/**
 * Just a page to make sure the server is running
 */
server.get("/", (req, res) => {
    res.send("auth0");
});

//=========================================================
// Bot Dialogs
//=========================================================
bot.dialog('/', new builder.IntentDialog()
    .matches(/^hello/i, "/hello")
    .matches(/^data/i, "/data")
    .matches(/^logout/i, "/logout")
    .onDefault((session, args) => {
        session.endDialog("I didn't understand that.  Try saying 'data'.");
    })
);

bot.dialog("/hello", (session, args) => {
    session.endDialog("Hello. I can help you get information from my service.  Try saying 'data'.");
});

bot.dialog("/data", [].concat(
    ba.authenticate("auth0"),
    function(session, results) {
        // ---------  code omitted
        // call my api behind gateway using jwt token... return data.
        // ---------  code omitted
        
       // let's just log the user if we get here for now
        var user = ba.profile(session, "auth0");
        console.log(user)
        
));

bot.dialog("/logout", [
    (session, args, next) => {
        builder.Prompts.confirm(session, "are you sure you want to logout");
    }, (session, args) => {
        if(args.response) {
            ba.logout(session, "auth0");
            session.endDialog("you've been logged out.");
        } else {
            session.endDialog("you're still logged in");
        }
    }
]);

//start the server
server.listen(PORT, function () {
   console.log('%s listening to %s', server.name, server.url);
});

OK,

Took me a bit but I figured out what I was doing wrong.

First, I needed to add a code page. Then, configure BotAuthenticator to redirect to to it at the end of the authentication phase.

server.get('/code', restify.serveStatic({'directory': path.join(__dirname, 'public'), 'file': 'code.html'}));
server.get('/styles.css', restify.serveStatic({'directory': path.join(__dirname, 'public'), 'file': 'styles.css'}));

var ba = new botauth.BotAuthenticator(server, bot, {
	// ...
	successRedirect: '/code',  // required to redirect to code page.
       //..
}).

See also the evernote and aadv2 examples: The takeaway here is that styles.css from the Evernote Example upper-cases the magic code...so when it is C&P'd, you have to lower case it to work.

After I got passed that, I experimented with sessions. I finally settled on the easy route of passing session: true, to the BotAuthenticator. However, since I was not creating a user in the Strategy callback, profile.user was always null preventing the magic because profile.user.id is required to be non-null for 'magic' to happen. I simply created a profile.user in the callback and added the information I needed from the profile (likely though I will use Auth0 rules to limit this in future).

I'll post back a link when I clean up the example and have instructions for creating the Auth0 tenants and clients.

Please feel free to close this.

P.S. really liking the library. We are beginning to expose API data with it, so excited to see where it goes.