Kent0111
Kent0111

Reputation: 147

Oauth2 Node Passport Authorization header error (Anonymous user)

Recently a Oauth 2 server we connect to was upgraded. In order for us to comply and get the new data we need to authenticate to this new server.

The previous server worked fine with the code I am about to lay out, however on the new server when we fully authenticate and get a consistent error.

This is the error

Client side error

app.js

All the passport magic happens here

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var config = require('./config.js')
var passport = require('passport');
var OAuth2Strategy = require('passport-oauth').OAuth2Strategy;
var session = require('express-session');
var async = require('async');

var index = require('./routes/index');
var users = require('./routes/users');
var patientdata = require('./routes/patient');
var account = require('./routes/account');
var logout = require('./routes/logout');

var about = require('./routes/about');


// serialize and deserialize
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
});

// config

passport.use('vendor', new OAuth2Strategy({
    authorizationURL: config.vendor.authorizationURL,
    tokenURL: config.vendor.tokenURL,
    clientID: config.vendor.clientID,
    clientSecret: config.vendor.clientSecret,
    callbackURL: config.vendor.callbackURL,
    passReqToCallback: true
    },
    function(req, accessToken, refreshToken, profile, done) {
        process.nextTick(function () {
            // store access token
            req.session.accessToken=accessToken;
            return done(null, profile);
        });
    }
));

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({secret: 'secretword'}))
app.use(passport.initialize());
app.use(passport.session());

app.get('/', index.execute);

app.get('/users', users.execute);
app.get('/account', ensureAuthenticated, account.execute);

app.get('/logout', logout.execute);

// vendor 
app.get('/auth/vendor',
passport.authenticate('vendor'),
function(req, res){
});

app.get('/auth/vendor/callback',
passport.authenticate('vendor', { failureRedirect: '/' }),
function(req, res) {
 res.redirect('/account');
}); 

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

function ensureAuthenticated(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect('/')
}

module.exports = app;

The info for URL,ID,secrets, etc is correct and is being verified by the vendor.

We appear to be authenticated to the Oauth2 server and we are getting an approved token, however we appear in their logs as "anonymous" user.

Here are the Log files from the vendor side.

Server log of my app attempting to authenticate

-- Us starting request for token
...
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 1 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 2 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 3 of 11 in additional filter chain; firing Filter: 'HeaderWriterFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@77bd892d
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/logout'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 6 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 7 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Populated SecurityContextHolder with anonymous token: 'org.springframework.security.authentication.AnonymousAuthenticationToken@6faab5ec: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffffc434: RemoteIpAddress: 68.46.8.80; SessionId: null; Granted Authorities: ROLE_ANONYMOUS'
Break -- returns anonymous

09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token'
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /oauth/token; Attributes: [fullyAuthenticated]
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@6faab5ec: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffffc434: RemoteIpAddress: 68.46.8.80; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
09:43:47.974 [http-nio-8282-exec-2] DEBUG o.s.s.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@73821302, returned: -1
09:43:47.975 [http-nio-8282-exec-2] DEBUG o.s.b.a.audit.listener.AuditListener - AuditEvent [timestamp=Thu Mar 24 09:43:47 CDT 2016, principal=anonymousUser, type=AUTHORIZATION_FAILURE, data={type=org.springframework.security.access.AccessDeniedException, message=Access is denied}]
09:43:47.975 [http-nio-8282-exec-2] DEBUG o.s.s.w.a.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point
...

I asked the vendor to login with a test app they made and send me the logs, see below that they have a "Basic authentication header"

Server log of vendor admin app authenticating

Break -- Starting  admin request for token
...
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 1 of 11 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 2 of 11 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 3 of 11 in additional filter chain; firing Filter: 'HeaderWriterFilter'
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.s.w.h.writers.HstsHeaderWriter - Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@77bd892d
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 4 of 11 in additional filter chain; firing Filter: 'LogoutFilter'
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/logout'
10:07:48.560 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 5 of 11 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
10:07:48.562 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Basic Authentication Authorization header found for user ' admin'
10:07:48.562 [http-nio-8282-exec-3] DEBUG o.s.s.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
10:07:48.563 [http-nio-8282-exec-3] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'scopedTarget.clientDetailsService'
10:07:48.564 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.w.BasicAuthenticationFilter - Authentication success: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@36677da1: Principal: org.springframework.security.core.userdetails.User@664f353: Username:  admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffff6a82: RemoteIpAddress: 66.41.28.185; SessionId: null; Granted Authorities: ROLE_CLIENT
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 6 of 11 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 7 of 11 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 8 of 11 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@36677da1: Principal: org.springframework.security.core.userdetails.User@664f353: Username:  admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffff6a82: RemoteIpAddress: 66.41.28.185; SessionId: null; Granted Authorities: ROLE_CLIENT'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 9 of 11 in additional filter chain; firing Filter: 'SessionManagementFilter'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.s.CompositeSessionAuthenticationStrategy - Delegating to org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy@285827ac
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 10 of 11 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.security.web.FilterChainProxy - /oauth/token at position 11 of 11 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.s.w.u.m.AntPathRequestMatcher - Checking match of request : '/oauth/token'; against '/oauth/token'
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /oauth/token; Attributes: [fullyAuthenticated]
10:07:48.580 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@36677da1: Principal: org.springframework.security.core.userdetails.User@664f353: Username:  admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_CLIENT; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffff6a82: RemoteIpAddress: 66.41.28.185; SessionId: null; Granted Authorities: ROLE_CLIENT
10:07:48.581 [http-nio-8282-exec-3] DEBUG o.s.s.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter@73821302, returned: 1
10:07:48.581 [http-nio-8282-exec-3] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Authorization successful
...

We know that we are not sending the "Authorization: BASIC (RANDOMCODE)" but we have been unable to manually inject this in. I was under the impression that passport did this for us but it appears it is not.

Lastly we went back and checked the old server and verified that we had never send this BASIC code in the header before. I am unsure how the server never caught this before but we need to upgrade soon and are stuck on how to get this working.

Upvotes: 2

Views: 1861

Answers (2)

Gordon Raup
Gordon Raup

Reputation: 21

Mwillbank, you are correct about the issue you address, but the problem here is in a different area. I work with the vendor in this issue. The issue is with the Authorization header in the getAccessToken call to the Authorization server during the authorization_code flow. The standard you referenced is regarding how to request data from a resource server once you have the access token. For that call you are absolutely correct and we implement that standard as you referenced. The portion of the OAuth2 standard applicable in our situation is Section 4.1.3 of the main standard (rfc6749). This section references Section 3.2.1 on Client Authentication, which in turn references Section 2.3, also on Client Authentication. Here you will see that there isn't a mandated standard for Client Authentication of Confidential Clients. However, the one example given, in 2.3.1 on the next page, is to use Basic Authentication. I believe this is the default and most commonly used method for authenticating clients during the getAccessToken call of the authorization_code flow.

Perhaps part of the issue is that passport.js is presuming this situation involved the implicit_grant flow between a mobile app and a web server, where there isn't a separate call to getAccessToken. I noticed in the links you posted a comment that said the passport implementation was mostly OAuth1 with spotty support for OAuth2. Perhaps we have run into one of those spots that were missed. Nevertheless I found your post helpful in understanding better how passport works. Thanks.

Upvotes: 1

mwillbanks
mwillbanks

Reputation: 1011

So with OAuth2 the expected token header is an Authorization: Bearer header. You can see this located here: https://www.rfc-editor.org/rfc/rfc6750#section-2.1 within the specification document.

In this case, the vendor is wrong if they are looking for the Basic header. Underneath the hoods passport.js oauth2 strategy is leveraging the following node.js module: https://github.com/ciaranj/node-oauth

To really start to dig into this at a deeper level, you will want to likely implement a sample by hand to see where things are getting "stuck". However, in looking through this, you can change the header from a bearer token to a basic token from the node-oauth library: https://github.com/ciaranj/node-oauth/blob/master/lib/oauth2.js#L15

To do this, we will need to change around a bit of code and access some areas that are likely far more susceptible to change since we're accessing internal properties.

First, change the strategy to push it into a variable:

var oauthStrategy = new OAuth2Strategy({
    authorizationURL: config.vendor.authorizationURL,
    tokenURL: config.vendor.tokenURL,
    clientID: config.vendor.clientID,
    clientSecret: config.vendor.clientSecret,
    callbackURL: config.vendor.callbackURL,
    passReqToCallback: true
    },
    function(req, accessToken, refreshToken, profile, done) {
        process.nextTick(function () {
            // store access token
            req.session.accessToken=accessToken;
            return done(null, profile);
        });
    }
);

Then, we need to access the "protected" property (it's not really protected since javascript cannot enforce that) - for more on this see: https://github.com/jaredhanson/passport-oauth2/blob/master/lib/strategy.js#L91.

oauthStrategy._oauth2.setAuthMethod('BASIC');

Now put back together the strategy:

passport.use('vendor', oauthStrategy);

I haven't actually tested this, but in looking through the source code this should work for you or at a minimum get you on the correct path.

Upvotes: 3

Related Questions