├── .gitignore ├── .netlify ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── Footer.js ├── Loading.js ├── OAuth.js ├── config.js ├── index.js └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | todos.txt 24 | -------------------------------------------------------------------------------- /.netlify: -------------------------------------------------------------------------------- 1 | {"site_id":"09aaa0a1-b716-40fc-9f76-df1cbe8a9c75","path":"build"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Social Authentication Client 2 | 3 | ![React Social Auth](https://i.imgur.com/acA83LR.gif) 4 | 5 | ## Medium posts that detail this repo 6 | * [Twitter, Google, Facebook, Github version on Codeburst](https://medium.com/p/862d59583105) 7 | * [Twitter only version on ITNEXT](https://medium.com/p/2f6b7b0ee9d2) 8 | 9 | ## Getting Started 10 | 11 | ``` 12 | git clone https://github.com/funador/react-auth-client.git 13 | cd react-auth-client 14 | npm i && HTTPS=true npm start 15 | ``` 16 | 17 | ### Because of Facebook, https is required. Even in development. 18 | Facebook requires all apps interacting with their api (including those in development) to be served over https. This means you will need to run create-react-app in https mode. Plus set up certificates for your server. Go ahead and get yourself a cup of coffee. This could take a minute. 19 | 20 | #### OS X 21 | To add https to localhost [follow these instructions](https://medium.freecodecamp.org/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec). 22 | 23 | You will also need to manually add the https certificate to Chrome as [described here](https://www.comodo.com/support/products/authentication_certs/setup/mac_chrome.php). 24 | 25 | You may also need to open a seperate tab for https://localhost:8080 and accept the security warning before the client will push requests through. 26 | 27 | #### Windows 28 | Thanks to [Le Gui PPF](https://medium.com/@guillaume.bottius) for providing the following instructions for setting up SSL locally on Windows: 29 | 30 | "Hi thanks for the code ! I haven’t tried it yet cause I spent a whole day (hot minute huh !) figuring out how to generate proper ssl certificate with all Chrome requirements… for those who don’t want to lose their time => [https://serverfault.com/a/850961](https://serverfault.com/a/850961) and add: 31 | 32 | ``` 33 | echo authorityKeyIdentifier=keyid,issuer 34 | echo basicConstraints=CA:FALSE 35 | echo keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 36 | ``` 37 | 38 | to v3 req. 39 | 40 | Hope that works for you Windows folks! 41 | 42 | If you only want to use Twitter/Google/Github authentication (https is not required), follow the instructions in [this branch](https://github.com/funador/react-auth-client/tree/twitter-auth) 43 | 44 | ## Client 45 | 46 | Depending on your OS you will have to flag the HTTPS enviornment variable differently. Documentation for different operating systems is [here](https://facebook.github.io/create-react-app/docs/using-https-in-development). 47 | 48 | ## Server 49 | 50 | Follow the instructions in the [server repo](https://github.com/funador/react-auth-server) 51 | 52 | Finally open https://localhost:3000 53 | 54 | #### Deploy 55 | Everything is set up to deploy to Netlify. You just need to `npm run build` on the client and deploy to netlify. 56 | 57 | ### Issues 58 | 59 | Something not working? Please [open an issue](https://github.com/funador/react-auth-client/issues) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social-auth-client", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "prop-types": "^15.6.2", 6 | "react": "^16.3.2", 7 | "react-dom": "^16.3.2", 8 | "react-fontawesome": "^1.6.1", 9 | "react-scripts": "2.1.3", 10 | "socket.io-client": "^2.1.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "dev": "HTTPS=true react-scripts start", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject", 18 | "deploy": "npm run build && netlify deploy" 19 | }, 20 | "browserslist": [ 21 | ">0.2%", 22 | "not dead", 23 | "not ie <= 11", 24 | "not op_mini all" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funador/react-auth-client/828aff3648f474f713897b312c83959739a7fda0/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 24 | Social Authentication with React 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto, sans-serif; 3 | background: #F2F2F2; 4 | } 5 | 6 | /* General Animations */ 7 | .fadein-slow { 8 | animation: fadein 4s; 9 | } 10 | 11 | .fadein-fast { 12 | animation: fadein 2s; 13 | } 14 | 15 | @keyframes fadein { 16 | from { 17 | opacity: 0; 18 | } 19 | to { 20 | opacity: 1; 21 | } 22 | } 23 | 24 | /* wrappers & containers */ 25 | .wrapper { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | } 30 | 31 | .container { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | justify-content: space-around; 36 | height: 87vh; 37 | width: 90vw; 38 | } 39 | 40 | @media screen and (max-width: 900px) { 41 | .container { 42 | flex-wrap: wrap 43 | } 44 | } 45 | 46 | @media screen and (max-width: 480px) { 47 | .container { 48 | flex-direction: column; 49 | flex-wrap: nowrap; 50 | height: 200vh; 51 | } 52 | } 53 | 54 | /* card */ 55 | .card { 56 | background-color: #FFF; 57 | border-radius: 3%; 58 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 59 | word-wrap: break-word; 60 | width: 215px; 61 | height: 100%; 62 | margin-bottom: 20px; 63 | transition: .5s; 64 | } 65 | 66 | .card:hover { 67 | box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.45); 68 | } 69 | 70 | .close { 71 | border-radius: 50%; 72 | text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25); 73 | float: right; 74 | top: -228px; 75 | right: -6px; 76 | font-size: 2em; 77 | position: relative; 78 | color: #fff; 79 | transition: .5s; 80 | } 81 | 82 | .close:hover { 83 | cursor: pointer; 84 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25); 85 | } 86 | 87 | img { 88 | width: 215px; 89 | border-radius: 3% 3% 0 0; 90 | } 91 | 92 | h4 { 93 | font-size: 1.2em; 94 | margin: 15px; 95 | color: #757575; 96 | } 97 | 98 | /* Loading Icon */ 99 | .loading-wrapper, .loading { 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | flex-direction: column; 104 | } 105 | 106 | .loading { 107 | width: 200px; 108 | height: 200px; 109 | } 110 | 111 | .loading .background { 112 | border-radius: 50%; 113 | background: #6762a6; 114 | border: 3px solid #c9c3e6; 115 | box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 116 | width: 200px; 117 | height: 200px; 118 | box-sizing: border-box; 119 | animation: pulse-colors 2s infinite alternate linear; 120 | } 121 | 122 | .loading i { 123 | margin: 25px 5px 5px 55px; 124 | float: left; 125 | font-size: 10em !important; 126 | color: #fff; 127 | animation: pulse-icon 2s infinite alternate linear; 128 | } 129 | 130 | @keyframes pulse-icon { 131 | from { 132 | text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 133 | } 134 | to { 135 | text-shadow: 2px 4px 4px rgba(0, 0, 0, 0.55); 136 | } 137 | } 138 | 139 | @keyframes pulse-colors { 140 | from { 141 | background: #c9c3e6; 142 | border: 3px solid #a29ccc; 143 | } 144 | to { 145 | background: #6762a6; 146 | border: 3px solid #c9c3e6; 147 | } 148 | } 149 | 150 | /* Button reset */ 151 | button { 152 | background: none; 153 | color: inherit; 154 | border: none; 155 | padding: 0; 156 | font: inherit; 157 | cursor: pointer; 158 | outline: inherit; 159 | margin-bottom: 20px; 160 | } 161 | 162 | /* Shared button styles */ 163 | .button-wrapper { 164 | height: 300px; 165 | } 166 | 167 | button { 168 | border-radius: 50%; 169 | width: 215px; 170 | height: 215px; 171 | box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 172 | transition-timing-function: ease-in; 173 | transition: 0.3s; 174 | transform: scale(0.7); 175 | } 176 | 177 | button:hover { 178 | box-shadow: 2px 5px 5px rgba(0, 0, 0, 0.5); 179 | } 180 | 181 | button.disabled { 182 | background-color: #999 !important; 183 | cursor: no-drop; 184 | } 185 | 186 | button.disabled:hover { 187 | box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 188 | } 189 | 190 | button.disabled:hover span { 191 | text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 192 | } 193 | 194 | button span { 195 | font-size: 10em !important; 196 | text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.25); 197 | transition: 0.3s; 198 | color: #fff; 199 | } 200 | 201 | button:hover span { 202 | text-shadow: 2px 5px 5px rgba(0, 0, 0, 0.5); 203 | transform: rotate(-1.1deg) 204 | } 205 | 206 | /* Twitter */ 207 | button.twitter { 208 | border: 3px solid #ffffff; 209 | background: #433e90 210 | } 211 | 212 | button.twitter:hover { 213 | background: #326ada; 214 | } 215 | 216 | /* Google */ 217 | button.google { 218 | border: 3px solid #ffffff; 219 | background: #0057e7; 220 | } 221 | 222 | button.google:hover { 223 | background: #008744; 224 | } 225 | 226 | /* Facebook */ 227 | button.facebook { 228 | border: 3px solid #ffffff; 229 | background: #8b9dc3; 230 | } 231 | 232 | button.facebook:hover { 233 | background: #3b5998; 234 | } 235 | 236 | /* Github */ 237 | button.github { 238 | border: 3px solid #ffffff; 239 | background: #767676; 240 | } 241 | 242 | button.github:hover { 243 | background: #6e5494; 244 | } 245 | 246 | /* footer */ 247 | footer { 248 | display: flex; 249 | justify-content: space-between; 250 | width: 87vw; 251 | } 252 | 253 | footer a { 254 | color: #fff; 255 | } 256 | 257 | footer span { 258 | font-size: 3em !important; 259 | text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.25); 260 | transition: .3s; 261 | padding-top: 3px; 262 | } 263 | 264 | footer span:hover { 265 | cursor: pointer; 266 | text-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 267 | } 268 | 269 | footer .small-button { 270 | border-radius: 50%; 271 | border: 1px solid #fff; 272 | width: 55px; 273 | height: 55px; 274 | transition: .3s; 275 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 276 | text-align: center; 277 | } 278 | 279 | footer .small-button:hover { 280 | background: #767676; 281 | box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.45); 282 | } 283 | 284 | footer .small-button.github { 285 | background: #6e5494; 286 | } 287 | 288 | footer .small-button.medium { 289 | background: #00ab6c; 290 | font-size: 0.85em; 291 | } 292 | 293 | footer .small-button.medium span { 294 | position: relative; 295 | top: 4px; 296 | } 297 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import io from 'socket.io-client' 3 | import OAuth from './OAuth' 4 | import Loading from './Loading' 5 | import Footer from './Footer' 6 | import { API_URL } from './config' 7 | import './App.css' 8 | const socket = io(API_URL) 9 | const providers = ['twitter', 'google', 'facebook', 'github'] 10 | 11 | export default class App extends Component { 12 | 13 | state = { 14 | loading: true 15 | } 16 | 17 | componentDidMount() { 18 | fetch(`${API_URL}/wake-up`) 19 | .then(res => { 20 | if (res.ok) { 21 | this.setState({ loading: false }) 22 | } 23 | }) 24 | } 25 | 26 | render() { 27 | const buttons = (providers, socket) => 28 | providers.map(provider => 29 | 34 | ) 35 | 36 | return ( 37 |
38 |
39 | {this.state.loading 40 | ? 41 | : buttons(providers, socket) 42 | } 43 |
44 |
45 |
46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /src/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | 4 | export default () => ( 5 | 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /src/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => 4 |
5 |

Heroku is spinning up, one moment please...

6 |
7 |
8 | 9 |
10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/OAuth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import FontAwesome from 'react-fontawesome' 4 | import { API_URL } from './config' 5 | 6 | export default class OAuth extends Component { 7 | 8 | state = { 9 | user: {}, 10 | disabled: '' 11 | } 12 | 13 | componentDidMount() { 14 | const { socket, provider } = this.props 15 | 16 | socket.on(provider, user => { 17 | this.popup.close() 18 | this.setState({user}) 19 | }) 20 | } 21 | 22 | checkPopup() { 23 | const check = setInterval(() => { 24 | const { popup } = this 25 | if (!popup || popup.closed || popup.closed === undefined) { 26 | clearInterval(check) 27 | this.setState({ disabled: ''}) 28 | } 29 | }, 1000) 30 | } 31 | 32 | openPopup() { 33 | const { provider, socket } = this.props 34 | const width = 600, height = 600 35 | const left = (window.innerWidth / 2) - (width / 2) 36 | const top = (window.innerHeight / 2) - (height / 2) 37 | const url = `${API_URL}/${provider}?socketId=${socket.id}` 38 | 39 | return window.open(url, '', 40 | `toolbar=no, location=no, directories=no, status=no, menubar=no, 41 | scrollbars=no, resizable=no, copyhistory=no, width=${width}, 42 | height=${height}, top=${top}, left=${left}` 43 | ) 44 | } 45 | 46 | startAuth = () => { 47 | if (!this.state.disabled) { 48 | this.popup = this.openPopup() 49 | this.checkPopup() 50 | this.setState({disabled: 'disabled'}) 51 | } 52 | } 53 | 54 | closeCard = () => { 55 | this.setState({user: {}}) 56 | } 57 | 58 | render() { 59 | const { name, photo} = this.state.user 60 | const { provider } = this.props 61 | const { disabled } = this.state 62 | const atSymbol = provider === 'twitter' ? '@' : '' 63 | 64 | return ( 65 |
66 | {name 67 | ?
68 | {name} 69 | 74 |

{`${atSymbol}${name}`}

75 |
76 | :
77 | 85 |
86 | } 87 |
88 | ) 89 | } 90 | } 91 | 92 | OAuth.propTypes = { 93 | provider: PropTypes.string.isRequired, 94 | socket: PropTypes.object.isRequired 95 | } 96 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.NODE_ENV === 'production' 2 | ? 'https://react-auth-twitter.herokuapp.com' 3 | : 'https://localhost:8080' -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import registerServiceWorker from './registerServiceWorker' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | registerServiceWorker() 8 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------