├── images ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png ├── screenshot6.png ├── screenshot7.png └── screenshot8.png ├── .gitignore ├── public ├── css │ └── style.css └── js │ ├── script.js │ ├── page.js │ ├── navbar.js │ └── api.js ├── src ├── routes │ ├── api.js │ └── views.js ├── openid_credentials.js ├── db.js ├── models │ └── user.js ├── views │ └── index.html ├── app.js └── passport.js ├── package.json ├── LICENSE └── README.md /images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot1.png -------------------------------------------------------------------------------- /images/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot2.png -------------------------------------------------------------------------------- /images/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot3.png -------------------------------------------------------------------------------- /images/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot4.png -------------------------------------------------------------------------------- /images/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot5.png -------------------------------------------------------------------------------- /images/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot6.png -------------------------------------------------------------------------------- /images/screenshot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot7.png -------------------------------------------------------------------------------- /images/screenshot8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertvunabandi/guide-on-mitopenid/HEAD/images/screenshot8.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /*.log 3 | node_modules/ 4 | *.swp 5 | secret.js 6 | package-lock.json 7 | cert.pem 8 | key.pem 9 | playground.js -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | #page { 2 | padding-top: 7rem; 3 | margin: 0 auto; 4 | font-weight: 900; 5 | text-align: center; 6 | font-size: 4rem; 7 | } 8 | 9 | .bg-primary, .navbar-dark, .navbar-expand-lg, .navbar { 10 | background-color: darkred !important; 11 | } -------------------------------------------------------------------------------- /src/routes/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // dependencies 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | router.get('/whoami', function (req, res) { 7 | res.send(req.isAuthenticated() ? req.user : {}); 8 | }); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /src/openid_credentials.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | id: 'your client id from https://oidc.mit.edu/', 4 | secret: 'your client secret from https://oidc.mit.edu/' 5 | }, 6 | auth: { 7 | tokenHost: 'https://oidc.mit.edu', 8 | tokenPath: '/token', 9 | authorizePath: '/authorize' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function main() { 4 | get('/api/whoami', {}, function (user) { 5 | renderNavbar(user); 6 | renderPage(user); 7 | }); 8 | } 9 | 10 | // run main only after the entire page has loaded: 11 | // this is better practice to avoid not being able 12 | // to find DOM nodes 13 | window.addEventListener('load', function () { 14 | main(); 15 | }); -------------------------------------------------------------------------------- /src/routes/views.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // dependencies 3 | const express = require('express'); 4 | const router = express.Router(); 5 | 6 | router.get('/', function (request, response) { 7 | response.sendFile(`index.html`, {root: 'src/views'}); 8 | }); 9 | 10 | router.get('/logout', function (req, res) { 11 | req.logout(); 12 | res.redirect('/'); 13 | }); 14 | 15 | module.exports = router; -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mongoose = require('mongoose'); 3 | const mongoURL = 'your mongoURI goes here'; 4 | const options = {useMongoClient: true}; 5 | 6 | mongoose.connect(mongoURL, options); 7 | mongoose.Promise = global.Promise; 8 | 9 | const db = mongoose.connection; 10 | 11 | // db error handling 12 | db.on('error', console.error.bind(console, 'MongoDB connection error:')); 13 | // db connection handling 14 | db.on('connected', () => console.log('database connected')); 15 | 16 | module.exports = db; 17 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // many of the parameters for this UserSchema are the ones 4 | // returned by MIT OpenID when the scopes are openid, 5 | // offline_access, email, and profile. Other parameters can 6 | // be added in case one asks for a scope number for example. 7 | const UserSchema = new mongoose.Schema({ 8 | name: String, 9 | given_name: String, 10 | middle_name: String, 11 | family_name: String, 12 | email: String, 13 | mitid: String 14 | }); 15 | 16 | module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /public/js/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function newPageItem(text) { 4 | const divBlock = document.createElement('div'); 5 | divBlock.innerHTML = text; 6 | return divBlock; 7 | } 8 | 9 | function renderPage(user) { 10 | const page = document.getElementById('page'); 11 | if (user._id) { 12 | page.appendChild(newPageItem("Now, you are logged in with MIT OpenID. Your name is")); 13 | page.appendChild(newPageItem("" + user.name + "")); 14 | } else { 15 | page.appendChild(newPageItem("You are NOT logged in")); 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitopenid", 3 | "version": "1.0.0", 4 | "description": "Implementing MIT OpenID", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon src/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "nodemon": "^1.14.11" 14 | }, 15 | "dependencies": { 16 | "body-parser": "^1.18.2", 17 | "cors": "^2.8.4", 18 | "express": "^4.16.2", 19 | "express-session": "^1.15.6", 20 | "http": "0.0.0", 21 | "mongoose": "^4.13.9", 22 | "morgan": "^1.9.0", 23 | "passport": "^0.4.0", 24 | "passport-mitopenid": "^1.0.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | How to MIT OpenID 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 |
21 |
22 |
23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /public/js/navbar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function newNavbarItem(text, url) { 4 | let itemLink = document.createElement('a'); 5 | itemLink.className = 'nav-item nav-link'; 6 | itemLink.innerHTML = text; 7 | itemLink.href = url; 8 | 9 | return itemLink; 10 | } 11 | 12 | function newNavbarItemTargetBlank(text, url) { 13 | let itemLink = document.createElement('a'); 14 | itemLink.className = 'nav-item nav-link'; 15 | itemLink.innerHTML = text; 16 | itemLink.href = url; 17 | itemLink.target = 'blank'; 18 | 19 | return itemLink; 20 | } 21 | 22 | function renderNavbar(user) { 23 | const navbarDiv = document.getElementById('nav-item-container'); 24 | navbarDiv.appendChild(newNavbarItem('Home', '/')); 25 | // NOTE: this check is a lowkey hack 26 | if (user._id) { 27 | navbarDiv.appendChild(newNavbarItem('Logout', '/logout')); 28 | } else { 29 | navbarDiv.appendChild(newNavbarItem('Login', '/auth/mitopenid')); 30 | } 31 | navbarDiv.appendChild(newNavbarItemTargetBlank('See Github', 'https://github.com/robertvunabandi/mitopenid')); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Robert M. Vunabndi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/js/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Source: 3 | // https://stackoverflow.com/questions/8064691/how-do-i-pass-along-variables-with-xmlhttprequest 4 | function formatParams(params) { 5 | return Object.keys(params).map(function (key) { 6 | return key + '=' + encodeURIComponent(params[key]); 7 | }).join('&'); 8 | } 9 | 10 | // params is given as a JSON 11 | function get(endpoint, params, successCallback, failureCallback) { 12 | const xhr = new XMLHttpRequest(); 13 | const fullPath = endpoint + '?' + formatParams(params); 14 | xhr.open('GET', fullPath, true); 15 | xhr.onload = function (error) { 16 | if (xhr.readyState === 4) { 17 | if (xhr.status === 200 && successCallback) { 18 | successCallback(JSON.parse(xhr.responseText)); 19 | } else if (failureCallback) { 20 | failureCallback(xhr.statusText); 21 | } 22 | } 23 | }; 24 | xhr.onerror = function (error) { 25 | failureCallback(xhr.statusText); 26 | }; 27 | xhr.send(null); 28 | } 29 | 30 | function post(endpoint, params, successCallback, failureCallback) { 31 | const xhr = new XMLHttpRequest(); 32 | xhr.open('POST', endpoint, true); 33 | xhr.setRequestHeader('Content-type', 'application/json'); 34 | xhr.withCredentials = true; 35 | xhr.onload = function (error) { 36 | if (xhr.readyState === 4) { 37 | if (xhr.status === 200 && successCallback) { 38 | successCallback(JSON.parse(xhr.responseText)); 39 | } else if (failureCallback) { 40 | failureCallback(xhr.statusText); 41 | } 42 | } 43 | }; 44 | xhr.onerror = function (error) { 45 | reject(xhr.statusText); 46 | }; 47 | xhr.send(JSON.stringify(params)); 48 | } 49 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // libraries 3 | const http = require('http'); 4 | const bodyParser = require('body-parser'); 5 | const express = require('express'); 6 | const session = require('express-session'); 7 | const cors = require('cors'); 8 | const morgan = require('morgan'); 9 | 10 | // local dependencies 11 | const db = require('./db'); 12 | const passport = require('./passport'); 13 | const views = require('./routes/views'); 14 | const api = require('./routes/api'); 15 | 16 | // initialize express app 17 | const app = express(); 18 | 19 | // set POST request body parser 20 | app.use(bodyParser.urlencoded({extended: false})); 21 | app.use(bodyParser.json()); 22 | 23 | // enable cross origin requests requests 24 | app.use(cors()); 25 | 26 | // morgan to log requests sent to this server 27 | // - 28 | // every time a request is sent to this server, 29 | // morgan logs that request with its method 30 | // (POST or GET), the status code (e.g. 200), 31 | // the url (e.g.: '/logout'), etc. You can see 32 | // the documentation for this with this link: 33 | // https://www.npmjs.com/package/morgan 34 | app.use(morgan(':method :status :url :response-time ms :res[content-length]')); 35 | 36 | // set up sessions 37 | app.use(session({ 38 | secret: 'session-secret', // <- make sure to make this a more secure secret 39 | resave: 'false', 40 | saveUninitialized: 'true' 41 | })); 42 | 43 | // hook up passport 44 | app.use(passport.initialize()); 45 | app.use(passport.session()); 46 | 47 | // set routes 48 | app.use('/', views); 49 | app.use('/api', api); 50 | app.use('/static', express.static('public')); 51 | 52 | // authentication routes 53 | // first route to make the authorization request and get the accessToken 54 | // the accessToken is a key that allows us to retrieve the requested 55 | // information from the server 56 | app.get('/auth/mitopenid', passport.authenticate('mitopenid')); 57 | // in the callback, we actually check if the user is logged in. If the 58 | // user is or is not, we take the appropriate action. usually, 59 | // failureRedirect is '/login', but we just send back to the home page 60 | app.get('/auth/mitopenid/callback', passport.authenticate('mitopenid', { 61 | successRedirect: '/', 62 | failureRedirect: '/' 63 | })); 64 | 65 | // 404 route 66 | app.use(function (req, res, next) { 67 | const err = new Error('Not Found'); 68 | err.status = 404; 69 | next(err); 70 | }); 71 | 72 | // route error handler 73 | app.use(function (err, req, res, next) { 74 | res.status(err.status || 500); 75 | res.send({ 76 | status: err.status, 77 | message: err.message, 78 | }); 79 | }); 80 | 81 | // port config 82 | const port = 3000; // config variable 83 | const server = http.Server(app); 84 | server.listen(port, function () { 85 | console.log('Server running on port: ' + port); 86 | }); -------------------------------------------------------------------------------- /src/passport.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // required for authentication 3 | const passport = require('passport'); 4 | const MITStrategy = require('passport-mitopenid').MITStrategy; 5 | 6 | // load the User model since the application user gets saved 7 | // within a function in this file 8 | const User = require('./models/user'); 9 | 10 | // if your app is deployed, change the host with whatever host 11 | // you have. A Heroku app host will look like: 12 | // https://mysterious-headland-54722.herokuapp.com 13 | const host = 'http://localhost:3000'; 14 | 15 | passport.use('mitopenid', new MITStrategy({ 16 | clientID: 'your client id from https://oidc.mit.edu/', 17 | clientSecret: 'your client secret from https://oidc.mit.edu/', 18 | callbackURL: host + '/auth/mitopenid/callback' 19 | }, function (accessToken, refreshToken, profile, done) { 20 | // uncomment the next line to see what your user object looks like 21 | // console.log(profile); 22 | 23 | // see comment at the end of file for what profile looks like 24 | // once we get the user's information from the request above, we need 25 | // to check if we have this user in our db. If not, we create this 26 | // user. 27 | User.findOne({mitid: profile.id}, function (err, user) { 28 | if (err) { 29 | return done(err); 30 | } else if (!user) { 31 | // if we don't find the user, that means this is the first 32 | // time this use is logging into our application, so, we 33 | // create this user. 34 | return createUser(); 35 | } else { 36 | return done(null, user); 37 | } 38 | }); 39 | 40 | // create the user using the mongoose model User 41 | function createUser() { 42 | const new_user = new User({ 43 | name: profile.name, 44 | given_name: profile.given_name, 45 | middle_name: profile.middle_name, 46 | family_name: profile.family_name, 47 | email: profile.email, 48 | mitid: profile.id 49 | }); 50 | new_user.save(function (err, user) { 51 | if (err) { 52 | return done(err); 53 | } 54 | return done(null, user); 55 | }); 56 | } 57 | })); 58 | 59 | // store the user's id into the user's session. store just the id 60 | // so that it's efficient. 61 | // see: http://www.passportjs.org/docs/configure/ 62 | passport.serializeUser(function (user, done) { 63 | done(null, user._id); 64 | }); 65 | 66 | // retrieve the id that we saved in the user's cookie session with 67 | // serializeUser, find that user with User.findById, then finally, 68 | // place that user inside of req.user with done(err, user) 69 | // see: http://www.passportjs.org/docs/configure/ 70 | passport.deserializeUser(function (id, done) { 71 | User.findById(id, function (err, user) { 72 | done(err, user); 73 | }); 74 | }); 75 | 76 | module.exports = passport; 77 | 78 | /* 79 | profile looks like: 80 | { 81 | id: String, 82 | name: String, 83 | preferred_username: String [Kerberos], 84 | given_name: String [First name], 85 | family_name: String, 86 | middle_name: String, 87 | email: String [the user's email], 88 | email_verified: Boolean [Not sure what this is] 89 | } 90 | 91 | That is, if the scopes (which are set on the client registration of 92 | MIT OpenID) are openid, offline_access, email, profile. offline_access 93 | is basically refreshToken, which are unlimited refreshing unless the 94 | user decides to revoke the app. 95 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HOW TO SET UP AND USE `MIT OpenID Connect Pilot` 2 | 3 | This guide will walk you through setting up [MIT OpenID Connect Pilot](https://oidc.mit.edu/) (**MOIDC**) for your app, which is a mean of providing authentication through kerberos for MIT students and affiliates, using [PassportJS](http://www.passportjs.org/) and the `npm` package [passport-mitopenid](https://www.npmjs.com/package/passport-mitopenid). 4 | 5 | The code provided in this guide is a skeleton code for an app (similar to [Catbook](https://github.com/mit6148-workshops/catbook)) with server-side material located in `./src` and client-side material located in `./public`. 6 | 7 | If you would like to simply integrate MIT OpenID into your app without following this skeleton, skip over to that the section on [**INTEGRATE `MIT OpenID` FROM AN EXISTING SOURCE CODE**](#integrate-mit-openid-from-an-existing-source-code). 8 | 9 | **NOTE:** This guide assumes that you are MIT affiliated. You will not be able to log into [MOIDC](https://oidc.mit.edu/) otherwise. This guide also assumes you are using `NodeJS` as an engine and running your server with `ExpressJS`. Finally, this guide assumes that you are using a MongoDB database. 10 | 11 | ## USE `MIT OpenID` FROM THIS SOURCE CODE 12 | 13 | This code will work correctly for `AuthorizationCode` Oauth 2.0 requests (see the section about [understanding How Oauth 2.0 Works](#understanding-how-oauth-20-works)). 14 | 15 | ### Step 1: Get your app's client credentials 16 | 17 | Go on [MOIDC](https://oidc.mit.edu/) and log in. You should be able to log in with your kerberos and kerberos password. 18 | 19 | Once logged, you should see the following screen: 20 | ![OIDC home page](images/screenshot1.png) 21 | 22 | Next step, go on `Self-service client registration` under the `Developer`'s section. Once you there, click on `Register a new client`. Then, you get the following screen: 23 | 24 | ![OIDC register a new client](images/screenshot2.png) 25 | 26 | Now, you need to complete all the necessary information. 27 | - `client name`: the name of your app: *this is the name people will see when they are prompted to authorize your app*. Make sure to give it a clear and "representative of your app" name. 28 | 29 | - `redirect URI(s)`: there are two cases: 30 | - If your app is under development (i.e. you are running it locally on your computer), put `http://localhost:{port}/`. 31 | - If your app is not under development or is deployed, you will put whatever path you were given from your deployment provider. For example, a [Heroku](https://www.heroku.com/) app will have a url similar to `https://mysterious-headland-54722.herokuapp.com/` (unless you have registered for your own domain), which is what you will put for `redirect URL(s)`. 32 | - You may also have a redirect `url` to a specific endpoint (say, `'/home'`). If so, use that then. 33 | 34 | - `Logo` (_optional_): Enter whatever URL you have for your application's logo. 35 | 36 | - Everything else is _optional_. Set your `application type` to be `Web`. When you're done, click on `Save`! 37 | 38 | It will refresh the page, and you should see something like this (**by the way, this app was deleted so it's pointless to attempt to copy the id and secret and RegistrationAccessToken**): 39 | ![OIDC register a new client](images/screenshot3.png) 40 | 41 | Once you have this and as it clearly says in **red**, **you MUST save your `ClientID`, `ClientSecret`, and `RegistrationAccessToken`**, otherwise you will not be able to access this screen again. So, make sure you save it! (I lost an app myself :confounded: ). 42 | 43 | Next and final thing to do is to go on the `Access` tab. You will see this: 44 | ![OIDC register a new client](images/screenshot4.png) 45 | 46 | Here's where you choose the scopes ([what are scopes?](https://docs.apigee.com/api-services/content/working-scopes)). There are many more scopes (which are suggested on keypress). They are simply what your application will need to access from the user. Put whatever you need there and **remove whatever you do not need** there (some users do not like to share certain information, such as their phone number, so removing what you do not need makes users more likely to use your application since it means they'd be sharing less data). 47 | 48 | Below that, make sure to toggle `refresh token`. This will be needed by `passportJS`! (I'm actually not entirely sure of this). 49 | 50 | Also, the grant type has to stay `authorization code`!!! Otherwise, this will not work. You will need to use a completely (semi-completely) different way of authentication. Also, those other grant types serve a different purpose (which you can understand if you look through the guide below). 51 | 52 | Then, you can ignore everything else and click on `Save`. Now, you should be ready to move onto **STEP 2**. 53 | 54 | 55 | ### Step 2: Clone this source code and fix the code accordingly 56 | 57 | 1. Clone this source code. Then, open `./src/passport.js`. In this file, replace wherever necessary with your new `clientID` and `ClientSecret`. 58 | 59 | 2. Then, open `./src/db.js` and enter your MongoDB URI from [MLAB](https://mlab.com/) (or any other database resource you may have). 60 | 61 | 3. *If you have already deployed your app:* Change the host in `./src/passport.js` to the right host. There is a comment about this. *Also, make sure your host doesn't include any "/" at the end*. If you have not deployed it, make sure to use the correct port. 62 | 63 | 4. Finally, the last thing is to modify your `User` model. Go into `user.js` and remove the parameters that are not needed and add whatever is needed. 64 | 65 | Now, you may ask, **"How do I know what's needed (i.e. what is in this user object that I receive from MIT's OpenID)?"** Although MIT OpenID doesn't have documentation that specify these things, you can test this out with modifying `./src/passport.js`. Inside of the function `passport.use('mitopenid', new MITStrategy ...`, there is a comment that says `// uncomment the next line to see what your user object looks like`. This may cause an error as your server handles the request, but it's okay because (in theory) you will fix your user model to have only the parameters you requested (which are given in that object). 66 | 67 | Now you should be good! Make sure to run `npm install` when you are ready to run your app. 68 | 69 | Your app should look like this: 70 | ![home page not logged in](images/screenshot5.png) 71 | And lead you to this page after you click on login: 72 | ![login with MIT OpenID](images/screenshot6.png) 73 | Then in this page, you confirm that you want to add the app: 74 | ![confirmation page to log authorize client application](images/screenshot7.png) 75 | Then when you're logged in, it will take you here: 76 | ![home page logged in](images/screenshot8.png) 77 | 78 | ## INTEGRATE `MIT OpenID` FROM AN EXISTING SOURCE CODE 79 | 80 | To integrate MIT OpenID to your app, we need to make a set of assumptions about your file structure. 81 | 1. Your server files are in `./src`, and your public files are in `./public`. 82 | 2. Your main file (the file that runs when you type `npm start` on your console) is named `app.js` and is located in `./src/app.js`. 83 | 84 | For the rest of this guide: 85 | - `AppUser` refers to the user of your app, and 86 | - `AppClient` refers to your application's server 87 | 88 | If you are starting a new project, go to the section on [**USE `MIT OpenID` FROM THIS SOURCE CODE**](#use-mit-openid-from-this-source-code) instead. 89 | 90 | Finally, to implement this, we made use of the `npm` module [passport-mitopenid](https://www.npmjs.com/package/passport-mitopenid). 91 | 92 | ### Step 1: Install Dependencies 93 | 94 | Make sure you have installed all necessary dependencies. 95 | 96 | Run the following: 97 | 98 | npm install --save express-session passport passport-mitopenid 99 | 100 | What each of those do (which you do not need to know): 101 | - `express-session`: We use this to save the `AppUser`'s `AccessToken`, which is like a key that allows your `AppClient` to get information about that user, on the `AppUser`'s browser as a [web cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). 102 | - `passport`: [`PassportJS`](http://www.passportjs.org/) is our mean of authenticating the user. 103 | - [`passport-mitopenid`](https://www.npmjs.com/package/passport-mitopenid): MIT OpenID abides by the [Oauth 2.0](https://oauth.net/2/) protocol, so we need the passport's implementation Strategy of Oauth 2.0 for MOIDC. passport-mitopenid is just the right passport-strategy for this. 104 | 105 | ### Step 2: Hook Up the right endpoints to your frontend 106 | 107 | You need an endpoint on your front-end that allows a user to login with MIT OpenID. We'll name that endpoint `'/auth/mitopenid'`. 108 | 109 | #### 2.1: Set `Login` and `Logout` Endpoints on Your Frontend 110 | 111 | Set your `Login` button's `href` on your front-end to be `'/auth/mitopenid'` and set your `Logout` button's `href` to be `'/logout'`. Here's an example: 112 | 113 | ```javascript 114 | function newNavbarItem(text, url) { 115 | let itemLink = document.createElement('a'); 116 | itemLink.className = 'nav-item nav-link'; 117 | itemLink.innerHTML = text; 118 | itemLink.href = url; 119 | 120 | return itemLink; 121 | } 122 | 123 | function renderNavbar(user) { 124 | const navbarDiv = document.getElementById('nav-item-container'); 125 | navbarDiv.appendChild(newNavbarItem('Home', '/')); 126 | // NOTE: this check is a lowkey hack 127 | if (user._id) { 128 | navbarDiv.appendChild(newNavbarItem('Logout', '/logout')); 129 | } else { 130 | // set the login button's href to point to '/auth/oidc' 131 | navbarDiv.appendChild(newNavbarItem('Login', '/auth/mitopenid')); 132 | } 133 | } 134 | ``` 135 | 136 | Our `renderNavbar` method sets the login button's href to point to `'/auth/mitopenid'`. Since this app is running with a server, when a user clicks on this login button, it leads them to `http://localhost:3000/auth/mitopenid`, which we will implement on the backend later along with the `'/logout'` endpoint leading to `http://localhost:3000/logout`. 137 | 138 | If you have your own way of implementing your navbar (or if your login button is not on the navbar), just make sure to make a button with the `'a'` HTML tag that has `href='/auth/mitopenid'` so that it points to the correct endpoint on your backend to log the user in using MIT OpenID. 139 | 140 | #### 2.2: Run `'/api/whoami'` When Your Frontend Loads 141 | 142 | Finally, we need to run a get request to `'/whoami'` at the start of your app to get the `AppUser`'s credential and "log them" in (similar to what [Catbook](https://github.com/mit6148-workshops/catbook) did). For us, we make the get request inside of `./public/js/script.js` (which is named `./public/js/index.js` on the [Catbook](https://github.com/mit6148-workshops/catbook) app), which returns an empty object if the user is not logged in or returns a user object *from the MongoDB mLab database* if the user is logged in. 143 | 144 | I.e., we do the following on load: 145 | 146 | ```javascript 147 | get('/api/whoami', {}, function (user) { 148 | // do something with "user", which is {} if 149 | // the AppUser is not logged in and 150 | // { _id: ..., ...} if AppUser is logged in 151 | // for example: 152 | renderNavbar(user); 153 | renderPage(user); 154 | }); 155 | ``` 156 | 157 | **NOTE:** This was [Catbook](https://github.com/mit6148-workshops/catbook)'s way of implementing this part which turned out to be a bit hacky. There are multiple ways of differentiating a user's state from being "logged-in" to being "logged-out"/"not-logged-in". 158 | 159 | ### Step 3: Implement your Backend Endpoints 160 | 161 | To keep our work clean, we will split the work of authenticating the user into multiple files (and maybe folders). 162 | 163 | #### 3.1. Create `passport.js` 164 | 165 | We will need to create a file just to handle what [passport](http://www.passportjs.org/) is doing to log `AppUser` in. Create the file `passport.js` inside of your `./src` directory, and then paste the following code into it: 166 | 167 | ```javascript 168 | const passport = require('passport'); 169 | const MITStrategy = require('passport-mitopenid').MITStrategy; 170 | // checkpoint 1 171 | const User = require('./models/user'); 172 | // checkpoint 2 173 | const host = 'http://localhost:3000'; 174 | // checkpoint 3 175 | passport.use('mitopenid', new MITStrategy({ 176 | clientID: 'your client id from https://oidc.mit.edu/', 177 | clientSecret: 'your client secret from https://oidc.mit.edu/', 178 | callbackURL: host + '/auth/mitopenid/callback' 179 | }, function(accessToken, refreshToken, profile, done) { 180 | // checkpoint 4 181 | User.findOne({openid: profile.id}, function (err, user) { 182 | if (err) { 183 | return done(err); 184 | } else if (!user) { 185 | // checkpoint 5 186 | return createUser(); 187 | } else { 188 | return done(null, user); 189 | } 190 | }); 191 | // checkpoint 5 (continued) 192 | function createUser() { 193 | const new_user = new User({ 194 | name: profile.name, 195 | given_name: profile.given_name, 196 | middle_name: profile.middle_name, 197 | family_name: profile.family_name, 198 | email: profile.email, 199 | openid: profile.sub 200 | }); 201 | new_user.save(function (err, user) { 202 | if (err) { 203 | return done(err); 204 | } 205 | return done(null, user); 206 | }); 207 | } 208 | })); 209 | // checkpoint 6 210 | passport.serializeUser(function (user, done) { 211 | done(null, user); 212 | }); 213 | // checkpoint 7 214 | passport.deserializeUser(function (user, done) { 215 | done(null, user); 216 | }); 217 | 218 | module.exports = passport; 219 | ``` 220 | I put multiple `checkpoints` in the code above so that the comments do not clutter up the space. I explain those comments. More details below. 221 | 222 | - With `checkpoint 1`, I want to mention that it assumes that you are using [MongoDB](https://docs.mongodb.com/manual/) and `mongoose`. If you are not, you will need to figure out how to store the user and retrieve inside of the big function call right after `checkpoint 5`. 223 | 224 | - With `checkpoint 2`: This `host` variable is your app's `URL`. If your app is to be deployed, make sure to change it. An heroku `URL` would look like `https://mysterious-headland-54722.herokuapp.com` for example. That's the `URL` you would put there. *MAKE SURE **NOT** TO INCLUDE A `'/'` AT THE END!* 225 | 226 | - With `checkpoint 5`: In here, it creates the user from the [Mongoose model](http://mongoosejs.com/docs/models.html) `User`. Remember that a `mongoose model`has a specific set of things that it can be. Therefore, the way the user's object is created here matters. 227 | 228 | That, should be the only thing you may need to change in this file (unless you're using a different port number, then you will need to change that as well). 229 | 230 | Now, although I put `checkpoints` a bit everywhere, for each of those checkpoints, I actually put comments in this demo code. So, if you want to know what they are saying, you can go into the demo code's file for `passport.js` (i.e. this codebase, in `./src/passport.js`) and see what each of those checkpoints mean (I would even suggest taking that code instead of this one since this one has no comments). 231 | 232 | #### 3.2. Hook `passport.js` into `app.js` 233 | 234 | By now, your app is almost ready to run! 235 | 236 | Open your `./src/app.js` (or whichever file gets run when you run `npm start`, which you can see inside your `package.json`). 237 | 238 | We need to make sure [passport](http://www.passportjs.org/) actually runs. 239 | 240 | Wherever you import your libraries, add the following 241 | 242 | ```javascript 243 | const session = require('express-session'); 244 | ``` 245 | 246 | This imports the `express-session` library. 247 | 248 | Then, wherever you import your local dependencies, make sure to import the newly created `passport.js` file with 249 | 250 | ```javascript 251 | const passport = require('./passport.js'); 252 | ``` 253 | 254 | You can also just write 255 | 256 | ```javascript 257 | const passport = require('./passport'); 258 | ``` 259 | 260 | because `Javscript` will know it's a Javascript file. Also,that assumes that your `app.js` and `passport.js` live in the same folder. 261 | 262 | Then, before you set up your routes (for example, before placing `app.use('/', views);`), add the following lines: 263 | 264 | ```javascript 265 | // set up sessions 266 | app.use(session({ 267 | secret: 'session-secret', // <- make sure to make this a more secure secret 268 | resave: 'false', 269 | saveUninitialized: 'true' 270 | })); 271 | ``` 272 | 273 | As it says in the comments, make sure to change your secret to something more secure. 274 | 275 | Then, right after that (**and make sure this actually comes after the `session`'s app.use**), add the following lines: 276 | 277 | ```javascript 278 | app.use(passport.initialize()); 279 | app.use(passport.session()); 280 | ``` 281 | 282 | That will hook up [passport](http://www.passportjs.org/) with our app and starts its passport's session mechanism. 283 | 284 | Finally, the last thing we need to do here is implement the `authentication routes` (these are done using `app.get(...)` for `GET` requests sent to you `AppClient`). So, after you have set up your routes (for example, after placing `app.use('/', views);`), paste the following: 285 | 286 | ```javascript 287 | // authentication routes 288 | app.get('/auth/mitopenid', passport.authenticate('mitopenid')); 289 | app.get('/auth/mitopenid/callback', passport.authenticate('mitopenid', { 290 | successRedirect: '/', 291 | failureRedirect: '/' 292 | })); 293 | ``` 294 | 295 | I also added comments as to what they do in this codebase (so again, it's better to copy the file from this codebase than from these snippets). If you remember, we already called the `'/auth/mitopenid'` endpoints on our frontend. That's a good pattern to use: *use it in the front end assuming it exists, then implement it in the backend* (or you can do these in parallel if working in group). 296 | 297 | #### 3.3: Implement `'/whoami'` and `'/logout'` 298 | 299 | Assuming you use the endpoint `'/'` within the `./src/routes/views.js` file, go into that file and add the following line: 300 | 301 | ```javascript 302 | router.get('/logout', function (req, res) { 303 | req.logout(); 304 | res.redirect('/'); 305 | }); 306 | ``` 307 | 308 | `req.logout()` is a method that `passport` 'magically' adds into our request. This method effectively logs the user out. Then, the method `res.redirect('/');` sends the request to the `'/'` endpoint, which should (ideally) send the user to the home page (the page they first see whenever they go into the website for the first time). 309 | 310 | Now, assuming you also have an `./src/routes/api.js` folder for your apis, go into that file and add the following: 311 | 312 | ```javascript 313 | router.get('/whoami', function (req, res) { 314 | res.send(req.isAuthenticated() ? req.user : {}); 315 | }); 316 | ``` 317 | 318 | `req.isAuthenticated()` is another method that `passport` 'magically' adds into our request. This method checks if the user is logged in. If they are not logged in, it just returns `false` (it's a boolean return method). For us, we wanted to return an empty object in case our user was not logged in, which is what that [ternary](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) operation does. Somehow, the user's object is within the request object (i.e. `req.user`). That is also something [passport](http://www.passportjs.org/) adds 'magically': it "places" (quote because it doens't really place) the user object inside of `req.user` for every requests sent to this server. 319 | 320 | **What if I do not have an `./src/routes/api.js` file?** 321 | 322 | Simply go into your `./src/app.js` and add the following: 323 | 324 | ```javascript 325 | app.get('/whoami', function (req, res) { 326 | res.send(req.isAuthenticated() ? req.user : {}); 327 | }); 328 | ``` 329 | 330 | That assumes that somewhere in your code, you have: 331 | 332 | ```javascript 333 | const app = express(); 334 | ``` 335 | 336 | That also means that your will access the `'/whoami'` endpoint without the `'/api'` before it. 337 | 338 | **What if I do not have an `./src/app.js` file?** 339 | 340 | We assumed that you have it! Otherwise, how did you even get to this step? Some people name that file `./src/index.js` instead of `./src/app.js`. 341 | 342 | Anyway: Now, if you run `npm start`, your app (in theory) should be able to run and provide authentication with MIT OpenID. 343 | 344 | ## FOLLOWING THE `PassportJS` DOCUMENTATION 345 | 346 | How did I know how to use `passport` for this? I simply went on [passport](http://www.passportjs.org/docs/) and clicked on the `Oauth` section. I also copied a bit from [Catbook](https://github.com/mit6148-workshops/catbook). 347 | 348 | I highly suggest reading through these documentations. They make understanding [passport](http://www.passportjs.org/) a lot easier than plugging random stuffs into the code. 349 | 350 | ## UNDERSTANDING HOW `OAuth 2.0` WORKS 351 | 352 | Below is simply a compiled list of links that helped me understand how OAuth 2.0 works. 353 | 354 | After having gone through them, this whole [passport](http://www.passportjs.org/) 'magic' made sense to me! So, I think it's valuable that this 'magic' makes sense because you can better understand what the code is doing and easily integrate other authentication methods (e.g. Google, Twitter, etc) into your app and even have an app with multiple authentication methods. 355 | 356 | ### Guides That Assume Little Background 357 | 358 | I recommend reading / watching about OAuth 2.0 from many sources. That gives many different perspectives to understanding how to works with this protocol. I tried to put the links below in order of low assumptions to some assumptions. 359 | 360 | - [InterSystems Learning Services: OAuth 2.0: An Overview](https://www.youtube.com/watch?v=CPbvxxslDTU): Very good Youtube video. 361 | - [DBA Presents: What is OpenID, OAuth2 and Google Sign In?](https://www.youtube.com/watch?v=1M6gqoGiO2s): Another really good Youtube video. 362 | - [Okta](https://www.oauth.com/oauth2-servers/background/): This article explains it from ground up in a high level manner. It uses code in `php`, but that's not too much of a drawback. 363 | - [Le Deng: OAuth 2 Explained](https://www.youtube.com/watch?v=L1PDqJkedZ0): Kind of slow video, but also good. 364 | - [Tech Primers: What is OAuth2? How does OAuth2 work?](https://www.youtube.com/watch?v=bzGKgC3N7SY): Youtube video. 365 | 366 | ### Guides that assume some background 367 | 368 | I tried to put the links below in order of low assumptions to more assumptions. Some of these videos are long; if you are using Chrome, I'd suggest using an extension that allow you to increase video speed (like [Video Speed Controller](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk?hl=en)). 369 | 370 | - [Oracle Learning Library: OAuth Introduction and Terminology](https://www.youtube.com/watch?v=zEysfgIbqlg): Youtube video. 371 | - [Oracle Learning Library: OAuth Grant Types](https://www.youtube.com/watch?v=1ZX7554l8hY): Youtube video. 372 | - [Alex Bilbie: A Guide To OAuth 2.0 Grants](https://alexbilbie.com/guide-to-oauth-2-grants/): blog post. 373 | - [Oracle Learning Library: OAuth Codes And Tokens](https://www.youtube.com/watch?v=8CHpnTysVOo): Youtube video. 374 | - [Oracle Learning Library: An Introduction To OpenID Connect](https://www.youtube.com/watch?v=6DxRTJN1Ffo): Youtube video. 375 | - [Oracle Learning Library: OpenID Connect Flows](https://www.youtube.com/watch?v=WVCzv50BslE&t=289s): Youtube video. 376 | - [Google Developers: Google I/O 2012 - OAuth 2.0 for Identity and Data Access](https://www.youtube.com/watch?v=YLHyeSuBspI&t=2669s): This Youtube video uses python, but is very good. 377 | 378 | ### Guides that assume a lot background (or are kind of unrelated) 379 | 380 | - [O'Reilly - Safari: Express.js Middleware Demystified](https://www.safaribooksonline.com/blog/2014/03/10/express-js-middleware-demystified/): A blog post. 381 | - [Request NPM](https://www.npmjs.com/package/request): Documentations for `request` on `npm`. 382 | - [StackExchange: Should we store accesstoken in our database for oauth2?](https://security.stackexchange.com/questions/72475/should-we-store-accesstoken-in-our-database-for-oauth2) 383 | - [StackOverflow: How do I redirect in expressjs while passing some context?](https://stackoverflow.com/questions/19035373/how-do-i-redirect-in-expressjs-while-passing-some-context) 384 | - [Oauth 2.0 Official Website](https://oauth.net/2/) 385 | - [StackOverflow: OAuth2, Using POST and Yet… Method Not Allowed? 386 | ](https://stackoverflow.com/questions/44685286/oauth2-using-post-and-yet-method-not-allowed) 387 | - [Internet Engineering Task Force (IETF)](https://tools.ietf.org/html/rfc7519): This is probably the most in-depth source. This is where OAuth 2.0 protocol is defined, so if you really want to understand everything about it, you should go here. Also, it's surprisingly understandable! --------------------------------------------------------------------------------