Skip to content

Instantly share code, notes, and snippets.

@jboxman
Last active January 3, 2021 16:06
Show Gist options
  • Save jboxman/5ffbfd3bd9ccc591484a468bc5805156 to your computer and use it in GitHub Desktop.
Save jboxman/5ffbfd3bd9ccc591484a468bc5805156 to your computer and use it in GitHub Desktop.
Github OAuth 2.0 authorization grant code SPA integration - deprecated
const config = require('../../config/envs');
import React from 'react';
import propTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
Input,
Menu,
Dropdown,
Icon,
Image,
Checkbox
} from 'semantic-ui-react';
import loginTab from '../../util/openWindow';
import * as userActions from '../redux/userActions';
import withAuthStatus from './hoc/withAuthStatus';
class AppMenu extends React.Component {
constructor() {
super();
}
handleLogIn(e, {name}) {
const msg = loginTab(config.app.oauthUrl);
msg.then(user => {
this.props.userActions.injectUser(user);
});
}
handleLogOut(e, {name}) {
this.props.userActions.logoutUser();
}
render() {
const {isAuthenticated, currentUser} = this.props;
const trigger = (
<span>
<Icon name='user' />
{isAuthenticated ? currentUser.username : ''}
</span>);
const loginButton = isAuthenticated ?
<Menu.Item>
<Dropdown simple trigger={trigger}>
<Dropdown.Menu>
<Dropdown.Divider />
<Dropdown.Item onClick={this.handleLogOut.bind(this)}>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Item>
:
<Menu.Item name="Login" onClick={this.handleLogIn.bind(this)} />;
return (
<Menu>
<Menu.Menu position='right'>
{loginButton}
</Menu.Menu>
</Menu>
)
}
}
AppMenu.PropTypes = {
isAuthenticated: propTypes.bool,
currentUser: propTypes.object
};
const mapStateToProps = state => ({
isAuthenticated: state.currentUser.isAuthenticated,
currentUser: state.currentUser.user
});
const mapDispatchToProps = dispatch => ({
userActions: bindActionCreators(userActions, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(AppMenu);
// http://scottksmith.com/blog/2014/09/18/beer-locker-building-a-restful-api-with-node-username-and-password/
const passport = require('koa-passport');
const GitHubStrategy = require('passport-github2').Strategy;
const MockStrategy = require('./mock-strategy').Strategy;
const User = require('../backend/entities/users/userModel');
function onSuccess(accessToken, refreshToken, profile, done) {
// https://stackoverflow.com/questions/20431049/what-is-function-user-findorcreate-doing-and-when-is-it-called-in-passport
User.findOrCreate(
{
oauthId: profile.id
},
{
oauthId: profile.id,
oauthProvider: profile.provider,
email: profile.emails[0].value,
username: profile.username,
avatarUrl: profile._json.avatar_url
},
function(error, user, created) {
done(null, user.toJSON());
});
}
function strategyForEnvironment() {
let strategy;
switch(process.env.NODE_ENV) {
case 'production':
strategy = new GitHubStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL
}, onSuccess);
break;
default:
strategy = new MockStrategy('github', onSuccess);
}
return strategy;
}
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(async function(id, done) {
try {
const user = await User.findById(id).exec();
done(null, user.toJSON());
}
catch(err) {
done(err);
}
});
passport.use(strategyForEnvironment());
// From https://gist.github.com/gauravtiwari/2ae9f44aee281c759fe5a66d5c2721a2
// By https://gist.github.com/gauravtiwari
/* global window */
const popup = (myUrl) => {
const windowArea = {
width: Math.floor(window.outerWidth * 0.8),
height: Math.floor(window.outerHeight * 0.5),
};
if (windowArea.width < 1000) { windowArea.width = 1000; }
if (windowArea.height < 630) { windowArea.height = 630; }
windowArea.left = Math.floor(window.screenX + ((window.outerWidth - windowArea.width) / 2));
windowArea.top = Math.floor(window.screenY + ((window.outerHeight - windowArea.height) / 8));
const sep = (myUrl.indexOf('?') !== -1) ? '&' : '?';
const url = `${myUrl}${sep}`;
const windowOpts = `toolbar=0,scrollbars=1,status=1,resizable=1,location=1,menuBar=0,
width=${windowArea.width},height=${windowArea.height},
left=${windowArea.left},top=${windowArea.top}`;
const authWindow = window.open(url, '_blank', windowOpts);
// Create IE + others compatible event handler
const eventMethod = window.addEventListener ? 'addEventListener' : 'attachEvent';
const eventer = window[eventMethod];
const messageEvent = eventMethod === 'attachEvent' ? 'onmessage' : 'message';
// Listen to message from child window
const authPromise = new Promise((resolve, reject) => {
eventer(messageEvent, (msg) => {
// This doesn't work in Chrome 59
//if (e.origin !== window.SITE_DOMAIN) {
// https://stackoverflow.com/questions/25098021/securityerror-blocked-a-frame-with-origin-from-accessing-a-cross-origin-frame
if (!~msg.origin.indexOf(`${window.location.protocol}//${window.location.host}`)) {
authWindow.close();
reject('Not allowed');
}
if (msg.data.payload) {
try {
resolve(JSON.parse(msg.data.payload));
}
catch(e) {
resolve(msg.data.payload);
}
finally {
authWindow.close();
}
} else {
authWindow.close();
reject('Unauthorised');
}
}, false);
});
return authPromise;
};
export default popup;
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login successful</title>
</head>
<body>
<h1>Success</h1>
<p>You are authenticated...</p>
</body>
<script>
document.body.onload = function() {
var injectedUser = <%- JSON.stringify(user) %>;
window.opener.postMessage(
{
payload: injectedUser,
status: 'success'
},
window.opener.location
);
/*
window.opener.postMessage(
{ error: 'Login failed' },
window.opener.location
);
*/
};
</script>
</html>
import * as ActionTypes from './userActionTypes';
export function injectUser(user = {}) {
return {
type: ActionTypes.INJECT,
payload: {
user
}
};
};
export function logoutUser() {
return {
type: ActionTypes.LOGOUT
}
};
export const INJECT = 'INJECT_USER';
export const LOGOUT = 'LOGOUT_USER';
const mongoose = require('mongoose');
const findOrCreate = require('mongoose-findorcreate');
const userSchema = mongoose.Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
avatarUrl: {
type: String
},
oauthId: {
type: String,
required: true,
select: false
},
oauthProvider: {
type: String,
required: true,
enum: ['github'],
select: false
}
});
// https://stackoverflow.com/questions/20431049/what-is-function-user-findorcreate-doing-and-when-is-it-called-in-passport
userSchema.plugin(findOrCreate);
module.exports = mongoose.model('User', userSchema);
import * as actionTypes from './userActionTypes';
export const STATE_KEY = 'currentUser';
export const initialState = {
isAuthenticated: false,
user: {}
};
export default function userReducer(state = initialState, action = {}) {
const {type, payload} = action;
switch(type) {
case actionTypes.INJECT:
return {
...state,
isAuthenticated: true,
user: payload.user
};
case actionTypes.LOGOUT:
return {
...initialState
};
default:
return state;
}
};
const Router = require('koa-router');
const passport = require('koa-passport');
const {
isAuthenticated
} = require('../../helpers');
const userModel = require('./userModel');
const router = new Router({
prefix: '/users'
});
router.get('/auth/github',
passport.authenticate('github')
);
// Custom handler that returns the authenticated user object
router.get('/auth/github/callback', function(ctx) {
return passport.authenticate('github', async function(err, user, info) {
await ctx.logIn(user);
await ctx.render('success', {user: JSON.stringify(ctx.state.user)});
})(ctx);
});
router.get('/auth/logout', isAuthenticated, function(ctx) {
ctx.logOut();
});
module.exports = router;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment