├── package.json ├── server.js ├── .gitignore ├── README.md └── model.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-oauth2-demo", 3 | "version": "1.0.0", 4 | "description": "oauth2 server using resource owner password credentials grant", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jeromyevans/node-oauth2-resource-owner-password-credentials-grant.git" 12 | }, 13 | "author": "Jeromy Evans", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/jeromyevans/node-oauth2-resource-owner-password-credentials-grant/issues" 17 | }, 18 | "homepage": "https://github.com/jeromyevans/node-oauth2-resource-owner-password-credentials-grant", 19 | "dependencies": { 20 | "body-parser": "^1.13.0", 21 | "express": "^4.12.4", 22 | "jsonwebtoken": "^5.0.2", 23 | "oauth2-server": "^2.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var oauthserver = require('oauth2-server'); 4 | var memorystore = require('./model.js'); 5 | 6 | var app = express(); 7 | 8 | app.use(bodyParser.urlencoded({ extended: true })); 9 | app.use(bodyParser.json()); 10 | 11 | app.oauth = oauthserver({ 12 | model: memorystore, 13 | grants: ['password', 'refresh_token'], 14 | debug: true, 15 | accessTokenLifetime: memorystore.JWT_ACCESS_TOKEN_EXPIRY_SECONDS, // expiry time in seconds, consistent with JWT setting in model.js 16 | refreshTokenLifetime: memorystore.JWT_REFRESH_TOKEN_EXPIRY_SECONDS // expiry time in seconds, consistent with JWT setting in model.js 17 | }); 18 | 19 | app.all('/oauth/token', app.oauth.grant()); 20 | 21 | app.get('/', app.oauth.authorise(), function (req, res) { 22 | res.send('Secret area'); 23 | }); 24 | 25 | app.use(app.oauth.errorHandler()); 26 | 27 | app.listen(3000); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/Node.gitignore 2 | # Logs 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # IntelliJ IDEA 31 | # https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion 33 | 34 | *.iml 35 | 36 | ## Directory-based project format: 37 | .idea/ 38 | # if you remove the above rule, at least ignore the following: 39 | 40 | # User-specific stuff: 41 | # .idea/workspace.xml 42 | # .idea/tasks.xml 43 | # .idea/dictionaries 44 | 45 | # Sensitive or high-churn files: 46 | # .idea/dataSources.ids 47 | # .idea/dataSources.xml 48 | # .idea/sqlDataSources.xml 49 | # .idea/dynamic.xml 50 | # .idea/uiDesigner.xml 51 | 52 | # Gradle: 53 | # .idea/gradle.xml 54 | # .idea/libraries 55 | 56 | # Mongo Explorer plugin: 57 | # .idea/mongoSettings.xml 58 | 59 | ## File-based project format: 60 | *.ipr 61 | *.iws 62 | 63 | ## Plugin-specific files: 64 | 65 | # IntelliJ 66 | /out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-build.properties 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | oAuth2 authorization server using Resource Owner Password Credentials Grant and a JWT bearer token 2 | ----------------------------------------------------------------------------------------- 3 | 4 | This grant type is used where the client is trusted by the resource owner (the user) and has a client id and 5 | client secret known by this server. *Trust* implies the user is willing to enter their username and password into the client, which 6 | usually means the client is issued or approved by the some organization that owns the authorization server. 7 | 8 | eg. the Twitter IOS app issued by Twitter, but not a third-party app what authenticates against Twitter's API. 9 | 10 | An IOS client use-case is: 11 | * The user would enter their username and password into the client. These do not need to be stored by the client (but it can use the IOS keystore). 12 | * The client authenticates (grant_type=password) against this server and receives an access token and refresh token. The 13 | client Id and client secret are also validated by this server. 14 | * The client uses the access token for all subsequent requests (http header Authorization: bearer ) 15 | * If the access token expires, the client requests a new one from this server (grant_type=refresh_token) 16 | and receives a new access token 17 | * The refresh token is retained by the client so the user doesn't have to login again. 18 | Once the refresh token has expired the user will need to login again (or start again using credentials in the keystore). 19 | 20 | The access token is generated using JWT. The benefits of this token are: 21 | * this server does not need to retain the tokens issued. It just needs to verify them. 22 | * the token can be passed straight through to other servers (eg. microservers) that only need to verify it (ie. they trust the signed JWT) 23 | * the token carries private claims that can be passed straight through to other servers (eg. user role/permission claims) 24 | 25 | The refresh token is also using JWT, although this probably isn't necessary. 26 | 27 | 28 | This demo just stores user and client credentials in memory. 29 | The token is generated using [node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). 30 | 31 | The examples is based on the in-memory example at https://github.com/thomseddon/node-oauth2-server/ but uses JWT tokens instead of the default implementation. 32 | 33 | 34 | -------------------------------------------------------------------------------- /model.js: -------------------------------------------------------------------------------- 1 | var JWT = require('jsonwebtoken'); 2 | 3 | var model = module.exports; 4 | 5 | // based on https://github.com/thomseddon/node-oauth2-server/tree/master/examples/memory 6 | 7 | var JWT_ISSUER = 'thisdemo'; 8 | var JWT_SECRET_FOR_ACCESS_TOKEN = 'XT6PRpRuehFsyMa2'; 9 | var JWT_SECRET_FOR_REFRESH_TOKEN = 'JWPVzFWkqGxoE2C2'; 10 | 11 | // the expiry times should be consistent between the oauth2-server settings 12 | // and the JWT settings (not essential, but makes sense) 13 | model.JWT_ACCESS_TOKEN_EXPIRY_SECONDS = 1800; // 30 minutes 14 | model.JWT_REFRESH_TOKEN_EXPIRY_SECONDS = 1209600; // 14 days 15 | 16 | // In-memory datastores 17 | var oauthClients = [{ 18 | clientId : 'thom', 19 | clientSecret : 'nightworld', 20 | redirectUri : '' 21 | }]; 22 | 23 | // key is grant_type 24 | // value is the array of authorized clientId's 25 | var authorizedClientIds = { 26 | password: [ 27 | 'thom' 28 | ], 29 | refresh_token: [ 30 | 'thom' 31 | ] 32 | }; 33 | 34 | // current registered users 35 | var users = [ { 36 | id : '123', 37 | username: 'thomseddon', 38 | password: 'nightworld' 39 | } 40 | ]; 41 | 42 | 43 | // Functions required to implement the model for oauth2-server 44 | 45 | // generateToken 46 | // This generateToken implementation generates a token with JWT. 47 | // the token output is the Base64 encoded string. 48 | model.generateToken = function(type, req, callback) { 49 | var token; 50 | var secret; 51 | var user = req.user; 52 | var exp = new Date(); 53 | var payload = { 54 | // public claims 55 | iss: JWT_ISSUER, // issuer 56 | // exp: exp, // the expiry date is set below - expiry depends on type 57 | // jti: '', // unique id for this token - needed if we keep an store of issued tokens? 58 | // private claims 59 | userId: user.id 60 | }; 61 | var options = { 62 | algorithms: ['HS256'] // HMAC using SHA-256 hash algorithm 63 | }; 64 | 65 | if (type === 'accessToken') { 66 | secret = JWT_SECRET_FOR_ACCESS_TOKEN; 67 | exp.setSeconds(exp.getSeconds() + model.JWT_ACCESS_TOKEN_EXPIRY_SECONDS); 68 | } else { 69 | secret = JWT_SECRET_FOR_REFRESH_TOKEN; 70 | exp.setSeconds(exp.getSeconds() + model.JWT_REFRESH_TOKEN_EXPIRY_SECONDS); 71 | } 72 | payload.exp = exp.getTime(); 73 | 74 | token = JWT.sign(payload, secret, options); 75 | 76 | callback(false, token); 77 | }; 78 | 79 | // The bearer token is a JWT, so we decrypt and verify it. We get a reference to the 80 | // user in this function which oauth2-server puts into the req object 81 | model.getAccessToken = function (bearerToken, callback) { 82 | 83 | return JWT.verify(bearerToken, JWT_SECRET_FOR_ACCESS_TOKEN, function(err, decoded) { 84 | 85 | if (err) { 86 | return callback(err, false); // the err contains JWT error data 87 | } 88 | 89 | // other verifications could be performed here 90 | // eg. that the jti is valid 91 | 92 | // we could pass the payload straight out we use an object with the 93 | // mandatory keys expected by oauth2-server, plus any other private 94 | // claims that are useful 95 | return callback(false, { 96 | expires: new Date(decoded.exp), 97 | user: getUserById(decoded.userId) 98 | }); 99 | }); 100 | }; 101 | 102 | 103 | // As we're using JWT there's no need to store the token after it's generated 104 | model.saveAccessToken = function (accessToken, clientId, expires, userId, callback) { 105 | return callback(false); 106 | }; 107 | 108 | // The bearer token is a JWT, so we decrypt and verify it. We get a reference to the 109 | // user in this function which oauth2-server puts into the req object 110 | model.getRefreshToken = function (bearerToken, callback) { 111 | return JWT.verify(bearerToken, JWT_SECRET_FOR_REFRESH_TOKEN, function(err, decoded) { 112 | 113 | if (err) { 114 | return callback(err, false); 115 | } 116 | 117 | // other verifications could be performed here 118 | // eg. that the jti is valid 119 | 120 | // instead of passing the payload straight out we use an object with the 121 | // mandatory keys expected by oauth2-server plus any other private 122 | // claims that are useful 123 | return callback(false, { 124 | expires: new Date(decoded.exp), 125 | user: getUserById(decoded.userId) 126 | }); 127 | }); 128 | }; 129 | 130 | // required for grant_type=refresh_token 131 | // As we're using JWT there's no need to store the token after it's generated 132 | model.saveRefreshToken = function (refreshToken, clientId, expires, userId, callback) { 133 | return callback(false); 134 | }; 135 | 136 | // authenticate the client specified by id and secret 137 | model.getClient = function (clientId, clientSecret, callback) { 138 | for(var i = 0, len = oauthClients.length; i < len; i++) { 139 | var elem = oauthClients[i]; 140 | if(elem.clientId === clientId && 141 | (clientSecret === null || elem.clientSecret === clientSecret)) { 142 | return callback(false, elem); 143 | } 144 | } 145 | callback(false, false); 146 | }; 147 | 148 | // determine whether the client is allowed the requested grant type 149 | model.grantTypeAllowed = function (clientId, grantType, callback) { 150 | callback(false, authorizedClientIds[grantType] && 151 | authorizedClientIds[grantType].indexOf(clientId.toLowerCase()) >= 0); 152 | }; 153 | 154 | // authenticate a user 155 | // for grant_type password 156 | model.getUser = function (username, password, callback) { 157 | for (var i = 0, len = users.length; i < len; i++) { 158 | var elem = users[i]; 159 | if(elem.username === username && elem.password === password) { 160 | return callback(false, elem); 161 | } 162 | } 163 | callback(false, false); 164 | }; 165 | 166 | var getUserById = function(userId) { 167 | for (var i = 0, len = users.length; i < len; i++) { 168 | var elem = users[i]; 169 | if(elem.id === userId) { 170 | return elem; 171 | } 172 | } 173 | return null; 174 | }; 175 | 176 | // for grant_type client_credentials 177 | // given client credentials 178 | // authenticate client 179 | // lookup user 180 | // return that user... 181 | // oauth replies with access token and renewal token 182 | //model.getUserFromClient = function(clientId, clientSecret, callback) { 183 | // 184 | //}; --------------------------------------------------------------------------------