The research is based on awesome PHP OAuth server solution proposed by Justin Greer. The idea under the PoC was to create simple php OAuth2 server to SSO with WordPress user account.
To do so, we need WordPress instance up and running. It might be Local installation as well to make it simple. We will create w PHP plugin to host OAuth2 API server and register a OAuth2 clients with ClientID/ClientSecret. To test PoC I will leverage Node,js application hosted on cloud server configured as a Generic OAuth2 client. The client be able to login with WordPress user account to Node.js application. The must-have feature is, it should be possible to map WP role to Node.js app user group as part of authorization process.
Flow diagram
The flow diagram below shows how exactly authorization process looks like:
The process consist of three main parts:
- Generic OAuth2 client calls /oauth/authorize endpoint, passes client_id and a nonce state parameter during authentication to protect against CSRF attacks.
- OAuth2 server generates code and calls back.
- The client asks for token sending code, grant_type which is ‘authorization_code’, client_id and client_secret to authorize against the server.
- Server generates short living token.
- Client calls server to get user data from WordPress database.
- Server retrieves logged user information and additional meta data like role
- OAuth2 server returns back user_claim
- Client maps user information and create or update user in Node.js database.
Configuration
OAuth2 server plugin for WordPress is available here on my GitHub. The essential part is API:
switch($method){
case 'authorize':
...
if ( !is_user_logged_in() ) {
wp_redirect( site_url() . '/wp-login/?sso_redirect='.$_GET['client_id'].'&state='.$_GET['state']);
exit();
}
...
case 'request_token':
...
case 'request_access':
...
$info = $wpdb->get_row("SELECT
u.ID
, u.user_login
, u.user_nicename
, u.user_email
, u.user_url
, u.user_registered
, u.user_status
, u.display_name
, m.meta_value AS role
FROM {$wpdb->prefix}users AS u
JOIN {$wpdb->prefix}usermeta AS m ON u.ID = m.user_id WHERE u.ID = ".$user_id."
AND m.meta_key = '{$wpdb->prefix}capabilities'");
// retrive user role, if not assign free role
$info->role = explode(" ", preg_match('/"(.*?)"/s', $info->role, $match) == 1 ? $match[1] : "free");
To use the plugin, pull repository and zip it. Go to WordPress admin panel, then plugins and upload new:
‘Provider’ menu entry should appear, open and generate a new client:
Client configuration as below:
Proof of concept
First of all, I have been registered a new user on my WordPress site:
Using separate account, logged in the nearly created account. then open node.js site login page and log with generic OAuth2 client.
First, it will generate a new temporary token in WP database:
Using the token we are able to get user_claim with Postman:
All, we need now is to specify claims as shown below in OAuth2 client strategy configuration panel:
Set up Node.js (or whatever application you use) OAuth2 client strategy using callback_url generated above. I am using passport module for node.js. Strategy will looks like:
const OAuth2Strategy = require('passport-oauth2').Strategy
module.exports = {
init (passport, conf) {
var client = new OAuth2Strategy({
authorizationURL: conf.authorizationURL,
tokenURL: conf.tokenURL,
clientID: conf.clientId,
clientSecret: conf.clientSecret,
userInfoURL: conf.userInfoURL,
callbackURL: conf.callbackURL,
passReqToCallback: true,
scope: conf.scope,
state: conf.enableCSRFProtection
}, async (req, accessToken, refreshToken, profile, cb) => {
try {
const user = await WIKI.models.users.processProfile({
providerKey: req.params.strategy,
profile: {
...profile,
id: _.get(profile, conf.userIdClaim),
displayName: _.get(profile, conf.displayNameClaim, '???'),
email: _.get(profile, conf.emailClaim)
}
})
if (conf.mapGroups) {
const groups = _.get(profile, conf.groupsClaim)
if (groups && _.isArray(groups)) {
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}
}
cb(null, user)
} catch (err) {
cb(err, null)
}
})
client.userProfile = function (accesstoken, done) {
this._oauth2._useAuthorizationHeaderForGET = !conf.useQueryStringForAccessToken
this._oauth2.get(conf.userInfoURL, accesstoken, (err, data) => {
if (err) {
return done(err)
}
try {
data = JSON.parse(data)
} catch (e) {
return done(e)
}
done(null, data)
})
}
passport.use(conf.key, client)
},
logout (conf) {
if (!conf.logoutURL) {
return '/'
} else {
return conf.logoutURL
}
}
}
After login successful, new user should be added as expected:
That is mostly it! I hope it was useful research done.
For more interesting content visit my #CyberTechTalk blog or follow me on Twitter.
Be an ethical, save your privacy!