├── .gitignore
├── views
├── error.pug
├── index.pug
├── layout.pug
├── tokencheck.pug
├── todos.pug
├── script.js
└── jwt-decode.js
├── .github
└── CODEOWNERS
├── .idea
├── misc.xml
├── .gitignore
└── modules.xml
├── public
└── stylesheets
│ └── style.css
├── routes
├── config.js
├── todos.js
├── todosapi.js
├── common.js
└── index.js
├── .env
├── fusionauth-example-modern-guide-to-oauth.iml
├── package.json
├── bin
└── www
├── README.md
├── app.js
├── docker-compose.yml
├── kickstart
├── kickstart.json
└── css
│ └── styles.css
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 |
4 | block content
5 | h1= title
6 | a(href='http://localhost:3000/login') Login
7 |
8 | p Welcome to #{title}
9 |
--------------------------------------------------------------------------------
/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a managed file. Manual changes will be overwritten.
2 | # https://github.com/FusionAuth/fusionauth-public-repos/
3 |
4 | .github/ @fusionauth/owners @fusionauth/platform
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; }
4 |
5 | a {
6 | color: #00B7FF; }
7 |
8 | /*# sourceMappingURL=style.css.map */
--------------------------------------------------------------------------------
/views/tokencheck.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 |
4 | block content
5 | h1= title
6 | a(href='http://localhost:3000/login') Login
7 |
8 | p Welcome to #{title}
9 |
10 | p Token state: #{active}
11 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/routes/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clientId : 'e9fdb985-9173-4e01-9d73-ac2d60d1dc8e',
3 | issuer : 'http://localhost:9011',
4 | clientSecret : 'super-secret-secret-that-should-be-regenerated-for-production',
5 | authServerUrl : 'http://localhost:9011',
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=postgres
2 | POSTGRES_PASSWORD=postgres
3 | DATABASE_USERNAME=fusionauth
4 | DATABASE_PASSWORD=hkaLBM3RVnyYeYeqE3WI1w2e4Avpy0Wd5O3s3
5 | ES_JAVA_OPTS="-Xms512m -Xmx512m"
6 | FUSIONAUTH_APP_MEMORY=512M
7 |
8 | FUSIONAUTH_APP_KICKSTART_FILE=/usr/local/fusionauth/kickstart/kickstart.json
9 |
--------------------------------------------------------------------------------
/routes/todos.js:
--------------------------------------------------------------------------------
1 | // Dependencies
2 | const express = require('express');
3 | const common = require('./common');
4 | const config = require('./config');
5 |
6 | // Router & constants
7 | const router = express.Router();
8 |
9 | router.get('/', (req, res, next) => {
10 | res.render('todos', {});
11 | });
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/views/todos.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 |
4 | block content
5 | a(href='http://localhost:3000/logout') Logout
6 |
7 | #content
8 |
9 | script(src='https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js')
10 | script(src='https://cdn.jsdelivr.net/npm/js-cookie@rc/dist/js.cookie.min.js')
11 | script
12 | include jwt-decode.js
13 | script
14 | include script.js
15 |
--------------------------------------------------------------------------------
/fusionauth-example-modern-guide-to-oauth.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fusionauth-example-modern-guide-to-oauth",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "axios": "^1.6.2",
10 | "cookie-parser": "~1.4.5",
11 | "debug": "~4.3.1",
12 | "express": "~4.17.1",
13 | "express-session": "~1.17.1",
14 | "form-data": "~3.0.0",
15 | "http-errors": "~1.8.0",
16 | "jsonwebtoken": "^9.0.2",
17 | "jwks-client": "^1.3.1",
18 | "jwks-rsa": "^3.1.0",
19 | "morgan": "~1.10.0",
20 | "pug": "~3.0.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const app = require('../app');
4 | const debug = require('debug')('fusionauth-modern-guide-to-oauth:server');
5 | const http = require('http');
6 |
7 | app.set('port', 3000);
8 |
9 | const server = http.createServer(app);
10 | server.listen(3000);
11 | server.on('error', onError);
12 | server.on('listening', onListening);
13 |
14 | function onError(error) {
15 | if (error.syscall !== 'listen') {
16 | throw error;
17 | }
18 |
19 | // handle specific listen errors with friendly messages
20 | switch (error.code) {
21 | case 'EADDRINUSE':
22 | console.error('Port 3000 is already in use');
23 | process.exit(1);
24 | break;
25 | default:
26 | throw error;
27 | }
28 | }
29 |
30 | function onListening() {
31 | const addr = server.address();
32 | const bind = typeof addr === 'string'
33 | ? 'pipe ' + addr
34 | : 'port ' + addr.port;
35 | debug('Listening on ' + bind);
36 | }
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FusionAuth Modern Guide to OAuth example application
2 |
3 | This project is an application that we build as part of our [Modern Guide to OAuth eBook](https://fusionauth.io/articles/oauth/modern-guide-to-oauth).
4 |
5 | ## Project Contents
6 |
7 | The `docker-compose.yml` file and the `kickstart` directory are used to start and configure a local FusionAuth server.
8 |
9 | ## Project Dependencies
10 |
11 | * Docker, for running FusionAuth
12 | * Node. Tested with version 20, but should work with any modern version.
13 |
14 | ## Running FusionAuth
15 | To run FusionAuth, just stand up the docker containers using `docker compose`.
16 |
17 | ```shell
18 | docker compose up
19 | ```
20 |
21 | This will start a PostgreSQL database, and Elastic service, and the FusionAuth server.
22 |
23 | ## Running the Example App
24 | To run the application, first install the modules
25 |
26 | ```shell
27 | npm install
28 | ```
29 | Then start the server.
30 |
31 | ```shell
32 | npm run start
33 | ```
34 |
35 | Visit the local webserver at `http://localhost:3000/` and sign in using the credentials:
36 |
37 | * username: richard@example.com
38 | * password: password
39 |
40 | You can also visit the FusionAuth admin at `http://localhost:9011` and sign in using the credentials:
41 |
42 | * username: admin@example.com
43 | * password: password
44 |
45 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const createError = require('http-errors');
2 | const cookieParser = require('cookie-parser');
3 | const express = require('express');
4 | const expressSession = require('express-session');
5 | const path = require('path');
6 | const logger = require('morgan');
7 |
8 | const indexRouter = require('./routes/index');
9 | const todosRouter = require('./routes/todos');
10 | const todosApiRouter = require('./routes/todosapi');
11 |
12 | const app = express();
13 |
14 | // view engine setup
15 | app.set('views', path.join(__dirname, 'views'));
16 | app.set('view engine', 'pug');
17 |
18 | app.use(logger('dev'));
19 | app.use(express.json());
20 | app.use(express.urlencoded({ extended: false }));
21 | app.use(cookieParser());
22 | app.use(expressSession({resave: false, saveUninitialized: false, secret: 'setec-astronomy'}));
23 | app.use(express.static(path.join(__dirname, 'public')));
24 |
25 | app.use('/', indexRouter);
26 | app.use('/todos', todosRouter);
27 | app.use('/api/todos', todosApiRouter);
28 |
29 | // catch 404 and forward to error handler
30 | app.use(function(req, res, next) {
31 | next(createError(404));
32 | });
33 |
34 | // error handler
35 | app.use(function(err, req, res, next) {
36 | // set locals, only providing error in development
37 | res.locals.message = err.message;
38 | res.locals.error = req.app.get('env') === 'development' ? err : {};
39 |
40 | // render the error page
41 | res.status(err.status || 500);
42 | res.render('error');
43 | });
44 |
45 | module.exports = app;
46 |
--------------------------------------------------------------------------------
/routes/todosapi.js:
--------------------------------------------------------------------------------
1 | // Dependencies
2 | const express = require('express');
3 | const common = require('./common');
4 | const config = require('./config');
5 | const axios = require('axios');
6 |
7 | // Router & constants
8 | const router = express.Router();
9 |
10 | router.get('/', (req, res, next) => {
11 | common.authorizationCheck(req, res).then((authorized) => {
12 | if (!authorized) {
13 | res.sendStatus(403);
14 | return;
15 | }
16 |
17 | const todos = common.getTodos();
18 | res.setHeader('Content-Type', 'application/json');
19 | res.end(JSON.stringify(todos));
20 | }).catch((err) => {
21 | console.log(err);
22 | });
23 | });
24 |
25 | router.post('/complete/:id', (req, res, next) => {
26 | common.authorizationCheck(req, res).then((authorized) => {
27 | if (!authorized) {
28 | res.sendStatus(403);
29 | return;
30 | }
31 |
32 | const idToUpdate = parseInt(req.params.id);
33 | common.completeTodo(idToUpdate);
34 |
35 | /*
36 | const wuphTokens = {}
37 | axios.post('https://api.wuphf.com/send', {}, { headers: {
38 | auth: { 'bearer': wuphfTokens.accessToken, 'refresh': wuphfTokens.refreshToken }
39 | }
40 | }).then((response) => {
41 | // check for status, log if not 200
42 | }
43 | );
44 | */
45 |
46 | const todos = common.getTodos();
47 | res.setHeader('Content-Type', 'application/json');
48 | res.end(JSON.stringify(todos));
49 | }).catch((err) => {
50 | console.log(err);
51 | });
52 | });
53 |
54 | module.exports = router;
55 |
--------------------------------------------------------------------------------
/views/script.js:
--------------------------------------------------------------------------------
1 | const buildAttemptRefresh = function(after) {
2 | return (error) => {
3 | console.log("trying to refresh");
4 | // try to refresh if we got an error
5 | // we can't send the cookie, so we need to request the refresh endpoint
6 | axios.post('/refresh', {})
7 | .then(function (response) {
8 | after();
9 | })
10 | .catch(function (error) {
11 | console.log("unable to refresh tokens");
12 | console.log(error);
13 | window.location.href="/";
14 | });
15 | };
16 | }
17 |
18 | const getTodos = function() {
19 | axios.get('/api/todos')
20 | .then(function (response) {
21 | buildUI(response.data);
22 | buildClickHandler();
23 | })
24 | .catch(console.log);
25 | }
26 |
27 | axios.get('/api/todos')
28 | .then(function (response) {
29 | buildUI(response.data);
30 | buildClickHandler();
31 | })
32 | .catch(buildAttemptRefresh(getTodos));
33 |
34 | function buildUI(data) {
35 | const todos = data;
36 | const id_token = Cookies.get('id_token');
37 | var decoded = jwt_decode(id_token);
38 | const email = decoded.email;
39 | const title = 'Todo list';
40 | var html = `
41 |
${title}
Todos for ${email}
42 | `;
53 |
54 | document.getElementById('content').innerHTML = html;
55 | }
56 |
57 | function buildClickHandler() {
58 | document.addEventListener('click', function (event) {
59 |
60 | // If the clicked element doesn't have the right selector, bail
61 | if (!event.target.matches('.chk')) return;
62 |
63 | // Log the clicked element in the console
64 | const completed = event.target.checked
65 | const id = event.target.dataset.id
66 | if (completed) {
67 | axios.post('/api/todos/complete/'+id, {})
68 | .then(function (response) {
69 | console.log(response);
70 | })
71 | .catch(buildAttemptRefresh(function() {
72 | axios.post('/api/todos/complete/'+id, {})
73 | .then(function (response) {
74 | console.log(response);
75 | })
76 | .catch(console.log);
77 | }));
78 | }
79 | }, false);
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: postgres:12.9
6 | environment:
7 | PGDATA: /var/lib/postgresql/data/pgdata
8 | POSTGRES_USER: ${POSTGRES_USER}
9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
10 | healthcheck:
11 | test: [ "CMD-SHELL", "pg_isready -U postgres" ]
12 | interval: 5s
13 | timeout: 5s
14 | retries: 5
15 | networks:
16 | - db_net
17 | restart: unless-stopped
18 | volumes:
19 | - db_data:/var/lib/postgresql/data
20 |
21 | search:
22 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
23 | environment:
24 | cluster.name: fusionauth
25 | bootstrap.memory_lock: "true"
26 | discovery.type: single-node
27 | ES_JAVA_OPTS: ${ES_JAVA_OPTS}
28 | healthcheck:
29 | test: [ "CMD", "curl", "--fail" ,"--write-out", "'HTTP %{http_code}'", "--silent", "--output", "/dev/null", "http://localhost:9200/" ]
30 | interval: 5s
31 | timeout: 5s
32 | retries: 5
33 | networks:
34 | - search_net
35 | restart: unless-stopped
36 | ulimits:
37 | memlock:
38 | soft: -1
39 | hard: -1
40 | volumes:
41 | - search_data:/usr/share/elasticsearch/data
42 |
43 | fusionauth:
44 | image: fusionauth/fusionauth-app:latest
45 | depends_on:
46 | db:
47 | condition: service_healthy
48 | search:
49 | condition: service_healthy
50 | environment:
51 | DATABASE_URL: jdbc:postgresql://db:5432/fusionauth
52 | DATABASE_ROOT_USERNAME: ${POSTGRES_USER}
53 | DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD}
54 | DATABASE_USERNAME: ${DATABASE_USERNAME}
55 | DATABASE_PASSWORD: ${DATABASE_PASSWORD}
56 | FUSIONAUTH_APP_MEMORY: ${FUSIONAUTH_APP_MEMORY}
57 | FUSIONAUTH_APP_RUNTIME_MODE: development
58 | FUSIONAUTH_APP_URL: http://fusionauth:9011
59 | SEARCH_SERVERS: http://search:9200
60 | SEARCH_TYPE: elasticsearch
61 | FUSIONAUTH_APP_KICKSTART_FILE: ${FUSIONAUTH_APP_KICKSTART_FILE}
62 |
63 | networks:
64 | - db_net
65 | - search_net
66 | restart: unless-stopped
67 | ports:
68 | - 9011:9011
69 | volumes:
70 | - fusionauth_config:/usr/local/fusionauth/config
71 | - ./kickstart:/usr/local/fusionauth/kickstart
72 |
73 | networks:
74 | db_net:
75 | driver: bridge
76 | search_net:
77 | driver: bridge
78 |
79 | volumes:
80 | db_data:
81 | fusionauth_config:
82 | search_data:
83 |
--------------------------------------------------------------------------------
/views/jwt-decode.js:
--------------------------------------------------------------------------------
1 | (function (factory) {
2 | typeof define === 'function' && define.amd ? define(factory) :
3 | factory();
4 | }((function () { 'use strict';
5 |
6 | /**
7 | * The code was extracted from:
8 | * https://github.com/davidchambers/Base64.js
9 | */
10 |
11 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
12 |
13 | function InvalidCharacterError(message) {
14 | this.message = message;
15 | }
16 |
17 | InvalidCharacterError.prototype = new Error();
18 | InvalidCharacterError.prototype.name = "InvalidCharacterError";
19 |
20 | function polyfill(input) {
21 | var str = String(input).replace(/=+$/, "");
22 | if (str.length % 4 == 1) {
23 | throw new InvalidCharacterError(
24 | "'atob' failed: The string to be decoded is not correctly encoded."
25 | );
26 | }
27 | for (
28 | // initialize result and counters
29 | var bc = 0, bs, buffer, idx = 0, output = "";
30 | // get next character
31 | (buffer = str.charAt(idx++));
32 | // character found in table? initialize bit storage and add its ascii value;
33 | ~buffer &&
34 | ((bs = bc % 4 ? bs * 64 + buffer : buffer),
35 | // and if not first of each 4 characters,
36 | // convert the first 8 bits to one ascii character
37 | bc++ % 4) ?
38 | (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) :
39 | 0
40 | ) {
41 | // try to find character in table (0-63, not found => -1)
42 | buffer = chars.indexOf(buffer);
43 | }
44 | return output;
45 | }
46 |
47 | var atob = (typeof window !== "undefined" &&
48 | window.atob &&
49 | window.atob.bind(window)) ||
50 | polyfill;
51 |
52 | function b64DecodeUnicode(str) {
53 | return decodeURIComponent(
54 | atob(str).replace(/(.)/g, function(m, p) {
55 | var code = p.charCodeAt(0).toString(16).toUpperCase();
56 | if (code.length < 2) {
57 | code = "0" + code;
58 | }
59 | return "%" + code;
60 | })
61 | );
62 | }
63 |
64 | function base64_url_decode(str) {
65 | var output = str.replace(/-/g, "+").replace(/_/g, "/");
66 | switch (output.length % 4) {
67 | case 0:
68 | break;
69 | case 2:
70 | output += "==";
71 | break;
72 | case 3:
73 | output += "=";
74 | break;
75 | default:
76 | throw "Illegal base64url string!";
77 | }
78 |
79 | try {
80 | return b64DecodeUnicode(output);
81 | } catch (err) {
82 | return atob(output);
83 | }
84 | }
85 |
86 | function InvalidTokenError(message) {
87 | this.message = message;
88 | }
89 |
90 | InvalidTokenError.prototype = new Error();
91 | InvalidTokenError.prototype.name = "InvalidTokenError";
92 |
93 | function jwtDecode(token, options) {
94 | if (typeof token !== "string") {
95 | throw new InvalidTokenError("Invalid token specified");
96 | }
97 |
98 | options = options || {};
99 | var pos = options.header === true ? 0 : 1;
100 | try {
101 | return JSON.parse(base64_url_decode(token.split(".")[pos]));
102 | } catch (e) {
103 | throw new InvalidTokenError("Invalid token specified: " + e.message);
104 | }
105 | }
106 |
107 | /*
108 | * Expose the function on the window object
109 | */
110 |
111 | //use amd or just through the window object.
112 | if (window) {
113 | if (typeof window.define == "function" && window.define.amd) {
114 | window.define("jwt_decode", function() {
115 | return jwtDecode;
116 | });
117 | } else if (window) {
118 | window.jwt_decode = jwtDecode;
119 | }
120 | }
121 |
122 | })));
123 | //# sourceMappingURL=jwt-decode.js.map
124 |
125 |
--------------------------------------------------------------------------------
/routes/common.js:
--------------------------------------------------------------------------------
1 |
2 | const axios = require('axios');
3 | const FormData = require('form-data');
4 | const config = require('./config');
5 | const { promisify } = require('util');
6 |
7 | const common = {};
8 |
9 | const jwksUri = config.authServerUrl+'/.well-known/jwks.json';
10 |
11 | const jwt = require('jsonwebtoken');
12 | const jwksClient = require('jwks-rsa');
13 | const client = jwksClient({
14 | strictSsl: true, // Default value
15 | jwksUri: jwksUri,
16 | requestHeaders: {}, // Optional
17 | requestAgentOptions: {}, // Optional
18 | timeout: 30000, // Defaults to 30s
19 | });
20 |
21 | common.parseJWT = async (unverifiedToken, nonce) => {
22 | const parsedJWT = jwt.decode(unverifiedToken, {complete: true});
23 | const getSigningKey = promisify(client.getSigningKey).bind(client);
24 | let signingKey = await getSigningKey(parsedJWT.header.kid);
25 | let publicKey = signingKey.getPublicKey();
26 | try {
27 | const token = jwt.verify(unverifiedToken, publicKey, { audience: config.clientId, issuer: config.issuer });
28 | if (nonce) {
29 | if (nonce !== token.nonce) {
30 | console.log("nonce doesn't match "+nonce +", "+token.nonce);
31 | return null;
32 | }
33 | }
34 | return token;
35 | } catch(err) {
36 | console.log(err);
37 | throw err;
38 | }
39 | }
40 |
41 | common.refreshJWTs = async (refreshToken) => {
42 | console.log("refreshing.");
43 | // POST refresh request to Token endpoint
44 | const form = new FormData();
45 | form.append('client_id', config.clientId);
46 | form.append('grant_type', 'refresh_token');
47 | form.append('refresh_token', refreshToken);
48 | const authValue = 'Basic ' + Buffer.from(config.clientId +":"+config.clientSecret).toString('base64');
49 | const response = await axios.post(config.authServerUrl+'/oauth2/token', form, {
50 | headers: {
51 | 'Authorization' : authValue,
52 | ...form.getHeaders()
53 | } });
54 |
55 | const accessToken = response.data.access_token;
56 | const idToken = response.data.id_token;
57 | const refreshedTokens = {};
58 | refreshedTokens.accessToken = accessToken;
59 | refreshedTokens.idToken = idToken;
60 | return refreshedTokens;
61 | }
62 |
63 | common.validateToken = async function (accessToken, clientId, expectedAud, expectedIss) {
64 |
65 | const form = new FormData();
66 | form.append('token', accessToken);
67 | form.append('client_id', clientId); // FusionAuth requires this for authentication
68 |
69 | try {
70 | const response = await axios.post(config.authServerUrl+'/oauth2/introspect', form, { headers: form.getHeaders() });
71 | if (response.status === 200) {
72 | const data = response.data;
73 | if (!data.active) {
74 | return false; // if not active, we don't get any other claims
75 | }
76 | return expectedAud === data.aud && expectedIss === data.iss;
77 | }
78 | } catch (err) {
79 | console.log(err);
80 | }
81 |
82 | return false;
83 | }
84 |
85 | common.retrieveUser = async function (accessToken) {
86 | const response = await axios.get(config.authServerUrl + '/oauth2/userinfo', { headers: { 'Authorization' : 'Bearer ' + accessToken } });
87 | try {
88 | if (response.status === 200) {
89 | return response.data;
90 | }
91 |
92 | return null;
93 | } catch (err) {
94 | console.log(err);
95 | }
96 | return null;
97 | }
98 |
99 | common.todos = [];
100 |
101 | common.completeTodo = (id) => {
102 | const todosToUpdate = common.getTodos();
103 | todosToUpdate.forEach((oneTodo) => {
104 | if (oneTodo.id === id) {
105 | oneTodo.done = true;
106 | }
107 | });
108 | }
109 |
110 | common.getTodo = (id) => {
111 | const todosToUpdate = common.getTodos();
112 | todosToUpdate.forEach((oneTodo) => {
113 | if (oneTodo.id === id) {
114 | return oneTodo;
115 | }
116 | });
117 |
118 | return null;
119 | }
120 |
121 | common.getTodos = () => {
122 | if (common.todos.length === 0) {
123 | // pull from the database in real world
124 | common.todos.push({'id': 1, 'task': 'Get milk', 'done' : true});
125 | common.todos.push({'id': 2, 'task': 'Read OAuth guide', 'done' : false});
126 | }
127 | return common.todos;
128 | }
129 |
130 | common.authorizationCheck = async (req, res) => {
131 | const accessToken = req.cookies.access_token;
132 | if (!accessToken) {
133 | return false;
134 | }
135 |
136 | try {
137 | let jwt = await common.parseJWT(accessToken);
138 | return true;
139 | } catch (err) {
140 | console.log(err);
141 | return false;
142 | }
143 | }
144 |
145 | module.exports = common;
146 |
--------------------------------------------------------------------------------
/kickstart/kickstart.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": {
3 | "apiKey": "33052c8a-c283-4e96-9d2a-eb1215c69f8f-not-for-prod",
4 | "asymmetricKeyId": "#{UUID()}",
5 | "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
6 | "clientSecret": "super-secret-secret-that-should-be-regenerated-for-production",
7 | "newThemeId": "#{UUID()}",
8 | "defaultTenantId": "d7d09513-a3f5-401c-9685-34ab6c552453",
9 | "adminEmail": "admin@example.com",
10 | "adminPassword": "password",
11 | "adminUserId": "00000000-0000-0000-0000-000000000001",
12 | "userEmail": "richard@example.com",
13 | "userPassword": "password",
14 | "userUserId": "00000000-0000-0000-0000-111111111111"
15 | },
16 | "apiKeys": [
17 | {
18 | "key": "#{apiKey}",
19 | "description": "Unrestricted API key"
20 | }
21 | ],
22 | "requests": [
23 | {
24 | "method": "POST",
25 | "url": "/api/key/generate/#{asymmetricKeyId}",
26 | "tenantId": "#{defaultTenantId}",
27 | "body": {
28 | "key": {
29 | "algorithm": "RS256",
30 | "name": "For exampleapp",
31 | "length": 2048
32 | }
33 | }
34 | },
35 | {
36 | "method": "POST",
37 | "url": "/api/application/#{applicationId}",
38 | "tenantId": "#{defaultTenantId}",
39 | "body": {
40 | "application": {
41 | "name": "Example App",
42 | "oauthConfiguration" : {
43 | "authorizedRedirectURLs": ["http://localhost:3000/oauth-callback"],
44 | "logoutURL": "http://localhost:3000/",
45 | "clientSecret": "#{clientSecret}",
46 | "enabledGrants": ["authorization_code", "refresh_token"],
47 | "requireRegistration": true
48 | },
49 | "jwtConfiguration": {
50 | "enabled": true,
51 | "accessTokenKeyId": "#{asymmetricKeyId}",
52 | "idTokenKeyId": "#{asymmetricKeyId}"
53 | }
54 | }
55 | }
56 | },
57 | {
58 | "method": "POST",
59 | "url": "/api/user/registration/#{adminUserId}",
60 | "body": {
61 | "registration": {
62 | "applicationId": "#{FUSIONAUTH_APPLICATION_ID}",
63 | "roles": [ "admin" ]
64 | },
65 | "roles": [ "admin" ],
66 | "skipRegistrationVerification": true,
67 | "user": {
68 | "birthDate": "1981-06-04",
69 | "data": {
70 | "favoriteColor": "chartreuse"
71 | },
72 | "email": "#{adminEmail}",
73 | "firstName": "Dinesh",
74 | "lastName": "Chugtai",
75 | "password": "#{adminPassword}"
76 | }
77 | }
78 | },
79 | {
80 | "method": "POST",
81 | "url": "/api/user/registration/#{userUserId}",
82 | "body": {
83 | "user": {
84 | "birthDate": "1985-11-23",
85 | "email": "#{userEmail}",
86 | "firstName": "Fred",
87 | "lastName": "Flintstone",
88 | "password": "#{userPassword}"
89 | },
90 | "registration": {
91 | "applicationId": "#{applicationId}",
92 | "data": {
93 | "favoriteColor": "turquoise"
94 | }
95 | }
96 | }
97 | },
98 | {
99 | "method": "POST",
100 | "url": "/api/theme/#{newThemeId}",
101 | "body": {
102 | "sourceThemeId": "75a068fd-e94b-451a-9aeb-3ddb9a3b5987",
103 | "theme": {
104 | "name": "React theme"
105 | }
106 | }
107 | },
108 | {
109 | "method": "PATCH",
110 | "url": "/api/theme/#{newThemeId}",
111 | "body": {
112 | "theme": {
113 | "stylesheet": "@{css/styles.css}"
114 | }
115 | }
116 | },
117 | {
118 | "method": "PATCH",
119 | "url": "/api/tenant/#{defaultTenantId}",
120 | "body": {
121 | "tenant": {
122 | "themeId": "#{newThemeId}",
123 | "issuer": "http://localhost:9011"
124 | }
125 | }
126 | }
127 | ]
128 | }
129 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | // Dependencies
2 | const express = require('express');
3 | const crypto = require('crypto');
4 | const axios = require('axios');
5 | const FormData = require('form-data');
6 | const common = require('./common');
7 | const config = require('./config');
8 |
9 | // Route and OAuth variables
10 | const router = express.Router();
11 | const clientId = config.clientId;
12 | const clientSecret = config.clientSecret;
13 | const redirectURI = encodeURI('http://localhost:3000/oauth-callback');
14 | const scopes = encodeURIComponent('profile offline_access openid');
15 |
16 | // Crypto variables
17 | const password = 'setec-astronomy'
18 | const key = crypto.scryptSync(password, 'salt', 24);
19 | const iv = crypto.randomBytes(16);
20 |
21 | router.get('/', (req, res, next) => {
22 | res.render('index', {title: 'FusionAuth Example'});
23 | });
24 |
25 | router.get('/logout', (req, res, next) => {
26 | removeTokens(res);
27 | req.session.destroy();
28 |
29 | // end FusionAuth session
30 | res.redirect(`${config.authServerUrl}/oauth2/logout?client_id=${config.clientId}`);
31 | });
32 |
33 | router.post('/refresh', async (req, res, next) => {
34 | const refreshToken = req.cookies.refresh_token;
35 | if (!refreshToken) {
36 | res.sendStatus(403);
37 | return;
38 | }
39 | try {
40 | const refreshedTokens = await common.refreshJWTs(refreshToken);
41 |
42 | const newAccessToken = refreshedTokens.accessToken;
43 | const newIdToken = refreshedTokens.idToken;
44 |
45 | // update our cookies
46 | console.log("updating our cookies");
47 | res.cookie('access_token', newAccessToken, {httpOnly: true, secure: true});
48 | res.cookie('id_token', newIdToken); // Not httpOnly or secure
49 | res.sendStatus(200);
50 | return;
51 | } catch (error) {
52 | console.log("unable to refresh");
53 | res.sendStatus(403);
54 | return;
55 | }
56 |
57 | });
58 |
59 |
60 | router.get('/tokencheck', async (req, res, next) => {
61 | const accessToken = req.cookies.access_token;
62 |
63 | const active = await common.validateToken(accessToken, config.clientId, config.clientId, config.issuer);
64 |
65 | res.render('tokencheck', {title: 'FusionAuth Example', active: active});
66 | });
67 |
68 | router.get('/login', (req, res, next) => {
69 | const state = generateAndSaveState(req, res);
70 | const codeChallenge = generateAndSaveCodeChallenge(req, res);
71 | const nonce = generateAndSaveNonce(req, res);
72 | res.redirect(302,
73 | config.authServerUrl + '/oauth2/authorize?' +
74 | `client_id=${clientId}&` +
75 | `redirect_uri=${redirectURI}&` +
76 | `state=${state}&` +
77 | `response_type=code&` +
78 | `scope=${scopes}&` +
79 | `code_challenge=${codeChallenge}&` +
80 | `code_challenge_method=S256&` +
81 | `nonce=${nonce}`);
82 | });
83 |
84 | router.get('/oauth-callback', (req, res, next) => {
85 | // Verify the state
86 | const reqState = req.query.state;
87 | const state = restoreState(req, res);
88 | if (reqState !== state) {
89 | res.redirect('/', 302); // Start over
90 | return;
91 | }
92 |
93 | const code = req.query.code;
94 | const codeVerifier = restoreCodeVerifier(req, res);
95 | const nonce = restoreNonce(req, res);
96 |
97 | // POST request to Token endpoint
98 | const form = new FormData();
99 | form.append('client_id', clientId);
100 | form.append('client_secret', clientSecret)
101 | form.append('code', code);
102 | form.append('code_verifier', codeVerifier);
103 | form.append('grant_type', 'authorization_code');
104 | form.append('redirect_uri', redirectURI);
105 | axios.post(config.authServerUrl+'/oauth2/token', form, { headers: form.getHeaders() })
106 | .then((response) => {
107 | const accessToken = response.data.access_token;
108 | const idToken = response.data.id_token;
109 | const refreshToken = response.data.refresh_token;
110 |
111 | // Parse and verify the ID token (it's a JWT and once verified we can just store the body)
112 | if (idToken) {
113 | let user = common.parseJWT(idToken, nonce);
114 | if (!user) {
115 | console.log('Nonce is bad. It should be ' + nonce + ' but was ' + idToken.nonce);
116 | res.redirect(302,"/"); // Start over
117 | return;
118 | }
119 | // TODO store user object in localstorage?
120 | }
121 |
122 |
123 | // Since the different OAuth modes handle the tokens differently, we are going to
124 | // put a placeholder function here. We'll discuss this function in the following
125 | // sections
126 | handleTokens(accessToken, idToken, refreshToken, res);
127 | }).catch((err) => {console.log("in error2"); console.error(JSON.stringify(err));});
128 | });
129 |
130 | // Helper method for Base 64 encoding that is URL safe
131 | function base64URLEncode(str) {
132 | return str.toString('base64')
133 | .replace(/\+/g, '-')
134 | .replace(/\//g, '_')
135 | .replace(/=/g, '');
136 | }
137 |
138 | function sha256(buffer) {
139 | return crypto.createHash('sha256')
140 | .update(buffer)
141 | .digest();
142 | }
143 |
144 | function encrypt(value) {
145 | const cipher = crypto.createCipheriv('aes-192-cbc', key, iv);
146 | let encrypted = cipher.update(value, 'utf8', 'hex');
147 | encrypted += cipher.final('hex');
148 | return encrypted + ':' + iv.toString('hex');
149 | }
150 |
151 | function decrypt(value) {
152 | const parts = value.split(':');
153 | const cipherText = parts[0];
154 | const iv = Buffer.from(parts[1], 'hex');
155 | const decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);
156 | let decrypted = decipher.update(cipherText, 'hex', 'utf8');
157 | decrypted += decipher.final('utf8');
158 | return decrypted;
159 | }
160 |
161 | function generateAndSaveState(req, res) {
162 | const state = base64URLEncode(crypto.randomBytes(64));
163 | res.cookie('oauth_state', encrypt(state), {httpOnly: true});
164 | return state;
165 | }
166 |
167 | function generateAndSaveCodeChallenge(req, res) {
168 | const codeVerifier = base64URLEncode(crypto.randomBytes(64));
169 | res.cookie('oauth_code_verifier', encrypt(codeVerifier), {httpOnly: true});
170 | return base64URLEncode(sha256(codeVerifier));
171 | }
172 |
173 | function generateAndSaveNonce(req, res) {
174 | const nonce = base64URLEncode(crypto.randomBytes(64));
175 | res.cookie('oauth_nonce', encrypt(nonce), {httpOnly: true});
176 | return nonce;
177 | }
178 |
179 | function restoreState(req, res) {
180 | const value = decrypt(req.cookies.oauth_state);
181 | res.clearCookie('oauth_state');
182 | return value;
183 | }
184 |
185 | function restoreCodeVerifier(req, res) {
186 | const value = decrypt(req.cookies.oauth_code_verifier);
187 | res.clearCookie('oauth_code_verifier');
188 | return value;
189 | }
190 |
191 | function restoreNonce(req, res) {
192 | const value = decrypt(req.cookies.oauth_nonce);
193 | res.clearCookie('oauth_nonce');
194 | return value;
195 | }
196 |
197 | function handleTokens(accessToken, idToken, refreshToken, res) {
198 |
199 | // Write the tokens as cookies
200 | res.cookie('access_token', accessToken, {httpOnly: true, secure: true});
201 | res.cookie('id_token', idToken); // Not httpOnly or secure
202 | res.cookie('refresh_token', refreshToken, {httpOnly: true, secure: true});
203 |
204 |
205 | // Call the third-party API
206 | /* this is fake, but you could replace with a real one
207 | axios.post('https://api.third-party-provider.com/profile/friends', form, { headers: { 'Authorization' : 'Bearer '+accessToken } })
208 | .then((response) => {
209 | if (response.status == 200) {
210 | const json = JSON.parse(response.data);
211 | req.session.friends = json.friends;
212 |
213 | // Optionally store the friends list in our database
214 | storeFriends(req, json.friends);
215 | }
216 | });
217 | */
218 |
219 | // Redirect to the To-do list
220 | res.redirect(302, '/todos');
221 |
222 | }
223 |
224 | function removeTokens(res) {
225 | // remove the token cookies
226 | res.cookie('access_token', null, {httpOnly: true, secure: true});
227 | res.cookie('id_token', null);
228 | res.cookie('refresh_token', null, {httpOnly: true, secure: true});
229 | }
230 |
231 | module.exports = router;
232 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/kickstart/css/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --main-text-color: #424242;
3 | --main-accent-color: #096324;
4 | --input-background: #fbfbfb;
5 | --body-background: #f7f7f7;
6 | --tooltip-background: #e2e2e2;
7 | --error-color: #ff0000;
8 | --error-background: #ffe8e8;
9 | --border-color: #dddddd;
10 | --logo-url: url(https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg);
11 | --font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
12 | }
13 | body {
14 | font-family: var(--font-stack);
15 | font-size: 16px;
16 | color: var(--main-text-color);
17 | background: var(--body-background);
18 | line-height: normal;
19 | }
20 | .page-body:before {
21 | content: '';
22 | display: block;
23 | width: 80%;
24 | max-width: 20rem;
25 | height: 3.5rem;
26 | margin: 0 auto 3rem auto;
27 | background-image: var(--logo-url);
28 | background-size: contain;
29 | background-position: center;
30 | background-repeat: no-repeat;
31 | }
32 |
33 | /* Changes for Powered by FusionAuth div */
34 | body > main main.page-body {
35 | min-height: calc(100vh - 3rem); /* to make the Powered by FusionAuth div position at the bottom of the page if the page is shorter than the viewport */
36 | padding-top: 3rem;
37 | }
38 | body > main {
39 | padding-bottom: 2.5rem; /* giving Powered by FusionAuth more space */
40 | }
41 | #powered-by-fa {
42 | position: absolute !important;
43 | }
44 | /* End Powered by FusionAuth */
45 |
46 |
47 | /* Hiding help bar at top */
48 | body > main {
49 | padding-top: 0;
50 | }
51 | /* end help bar */
52 |
53 |
54 | /* Typical typography */
55 | h1, h2, h3, h4, h5, h6 {
56 | line-height: normal;
57 | }
58 | p {
59 | margin: 1.5em 0;
60 | line-height: 1.375;
61 | }
62 | /* End typography */
63 |
64 |
65 | /* Typical Buttons and Links */
66 | a {
67 | color: var(--main-accent-color);
68 | text-decoration: underline;
69 | }
70 | a:hover {
71 | color: var(--main-accent-color);
72 | opacity: .8;
73 | }
74 | a:visited {
75 | color: var(--main-accent-color);
76 | }
77 | .blue-text {
78 | color: var(--main-accent-color) !important;
79 | }
80 | .form-row:last-of-type {
81 | margin-bottom: 0;
82 | }
83 | .button {
84 | font-size: 1.125rem !important;
85 | border-radius: .5rem;
86 | padding: 1rem !important;
87 | line-height: normal !important;
88 | letter-spacing: normal !important;
89 | }
90 | .button.blue {
91 | background: var(--main-accent-color) !important;
92 | width: 100%;
93 | margin-top: 2.5rem;
94 | }
95 | .button.blue:hover {
96 | opacity: .8 !important;
97 | background: var(--main-accent-color) !important;
98 | }
99 | .button.blue:focus {
100 | background: var(--main-accent-color) !important;
101 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.4),0 0 0 2px rgba(57,152,219,0.4);
102 | outline: 1px solid #ffffff !important;
103 | }
104 | .button.blue > .fa {
105 | display: none;
106 | }
107 | .secondary-btn,
108 | main.page-body .row:last-of-type a {
109 | text-decoration: none;
110 | padding: .5em .75em;
111 | border: 1px solid var(--main-accent-color);
112 | border-radius: .25em;
113 | font-size: .75rem;
114 | margin-top: .7rem;
115 | display: inline-block;
116 | line-height: normal;
117 | }
118 | .button + a {
119 | text-align: center;
120 | display: block;
121 | margin: 1em auto;
122 | }
123 | /* End buttons and links */
124 |
125 |
126 | /* Typical Form panel and inputs */
127 | .panel {
128 | box-shadow: 0 0 1.5625rem 1.25rem rgba(234, 234, 234, 0.8);
129 | border-radius: .625rem;
130 | border: none;
131 | padding: 2.25rem 2.75rem;
132 | }
133 | .panel h2,
134 | fieldset legend,
135 | legend {
136 | text-align: center;
137 | color: var(--main-accent-color);
138 | font-size: 1.5625rem;
139 | font-weight: 600;
140 | margin: 0 0 2rem 0;
141 | padding: 0;
142 | border: none;
143 | }
144 | legend {
145 | border: none;
146 | width: auto;
147 | }
148 | form .form-row {
149 | margin-bottom: 1.25rem;
150 | }
151 | label {
152 | color: var(--main-text-color);
153 | font-size: 1rem;
154 | font-weight: 500;
155 | }
156 | label.radio,
157 | label.checkbox {
158 | margin: 1rem 0;
159 | font-weight: 400;
160 | }
161 | .input-addon-group,
162 | .input-addon-group > :last-child:not(.flat),
163 | .input-addon-group > .input:last-child:not(.flat),
164 | .input-addon-group > input:last-child:not(.flat) {
165 | color: var(--main-text-color); /* overriding typical text color for inputs */
166 | }
167 | .input-addon-group span {
168 | display: none; /* Hiding icons on inputs */
169 | }
170 | input::placeholder {
171 | color: var(--main-text-color);
172 | }
173 | .input,
174 | input[type="email"],
175 | input[type="file"],
176 | input[type="number"],
177 | input[type="search"],
178 | input[type="text"],
179 | input[type="tel"],
180 | input[type="url"],
181 | input[type="password"],
182 | textarea,
183 | label.select select
184 | {
185 | background: var(--input-background);
186 | border: 1px solid var(--border-color) !important;
187 | border-radius: .25rem !important;
188 | box-shadow: none;
189 | font-size: 1rem;
190 | padding: 1em .625em;
191 | }
192 | input:focus,
193 | input:active,
194 | textarea:focus,
195 | textarea:active {
196 | border: 1px solid #707070 !important;
197 | box-shadow: none !important;
198 | }
199 | .radio input {
200 | width: 1.3125rem;
201 | height: 1.3125rem;
202 | }
203 | .radio span.box,
204 | .checkbox span.box {
205 | width: 1.3125rem;
206 | height: 1.3125rem;
207 | margin: 0;
208 | border: solid 1px var(--border-color);
209 | background-color: var(--input-background);
210 | }
211 | .radio span.box {
212 | border-radius: 50%;
213 | }
214 | .radio input:checked + span.box {
215 | border: 2px solid var(--main-accent-color);
216 | }
217 | .radio span.box::after {
218 | box-shadow: none;
219 | border-radius: 50%;
220 | background: var(--main-accent-color);
221 | width: .8125rem;
222 | height: .8125rem;
223 | top: .125rem;
224 | left: .125rem;
225 | }
226 | .radio span.box:hover::after {
227 | opacity: 0;
228 | }
229 | .radio span.label,
230 | .checkbox span.label {
231 | margin-left: .5rem;
232 | }
233 | .radio-items .form-row label span:last-of-type {
234 | border-color: var(--border-color);
235 | }
236 | input[type="radio"] {
237 | width: 1.3125rem;
238 | height: 1.3125rem;
239 | margin: 0;
240 | border: solid 1px var(--border-color);
241 | border-radius: 50%;
242 | background-color: var(--input-background);
243 | appearance: none;
244 | -webkit-appearance: none;
245 | vertical-align: text-bottom;
246 | }
247 | input[type="radio"]:focus,
248 | input[type="radio"]:active,
249 | input[type="radio"]:checked {
250 | border: 2px solid var(--main-accent-color) !important;
251 | }
252 | input[type="radio"]:checked:after {
253 | content: '';
254 | box-shadow: none;
255 | border-radius: 50%;
256 | background: var(--main-accent-color);
257 | width: .8125rem;
258 | height: .8125rem;
259 | top: .125rem;
260 | left: .125rem;
261 | position: absolute;
262 | }
263 | .checkbox span.box {
264 | border-radius: .25rem;
265 | }
266 | .checkbox input:checked + span.box {
267 | background: var(--main-accent-color);
268 | border-color: var(--main-accent-color);
269 | }
270 | .checkbox span.box::after {
271 | height: .25rem;
272 | left: .25rem;
273 | top: .3125rem;
274 | transform: rotate(-46deg);
275 | width: .625rem;
276 | box-shadow: none;
277 | }
278 | .checkbox-list {
279 | background: transparent;
280 | border: none;
281 | box-shadow: none;
282 | padding-left: 0;
283 | }
284 | input[type="checkbox"] {
285 | width: 1.3125rem;
286 | height: 1.3125rem;
287 | margin: 0;
288 | border: solid 1px var(--border-color);
289 | border-radius: .25rem;
290 | background-color: var(--input-background);
291 | appearance: none;
292 | -webkit-appearance: none;
293 | vertical-align: text-bottom;
294 | }
295 | input[type="checkbox"]:checked {
296 | background-color: var(--main-accent-color);
297 | }
298 | input[type="checkbox"]:checked:after {
299 | content: '';
300 | background: transparent;
301 | border: 2px solid #fff;
302 | border-right: none;
303 | border-top: none;
304 | height: .25rem;
305 | left: .25rem;
306 | top: .3125rem;
307 | transform: rotate(-46deg);
308 | width: .625 rem;
309 | display: block;
310 | position: absolute;
311 | }
312 | label.select select {
313 | color: var(--main-text-color);
314 | }
315 | label.select select option {
316 | background: var(--input-background);
317 | color: var(--main-text-color);
318 | }
319 | /* End Panel and Form Inputs */
320 |
321 |
322 | /* Errors */
323 | body .alert {
324 | color: var(--main-text-color);
325 | }
326 | body .alert a {
327 | height: auto;
328 | width: auto;
329 | }
330 | body .alert.error a i.fa {
331 | color: var(--error-color);
332 | }
333 | body .alert.error {
334 | border: 1px solid var(--error-color);
335 | margin: 0 0 2rem 0;
336 | background: var(--error-background);
337 | box-shadow: none;
338 | border-radius: .25rem;
339 | }
340 | body .alert .dismiss-button i {
341 | margin: 0;
342 | }
343 | .error {
344 | font-size: .75rem;
345 | margin: .5em 0;
346 | }
347 | label.error {
348 | color: var(--error-color);
349 | font-size: inherit;
350 | }
351 | form .form-row span.error {
352 | color: var(--error-color);
353 | }
354 | input.error {
355 | background: var(--error-background);
356 | border-color: var(--error-color) !important;
357 | }
358 | /* End Errors */
359 |
360 |
361 | /* Tooltip */
362 | .tooltip {
363 | background: var(--tooltip-background);
364 | font-size: .75rem;
365 | color: var(--main-text-color);
366 | text-align: left;
367 | }
368 | .tooltip:after {
369 | border-top-color: var(--tooltip-background);
370 | }
371 | .tooltip.inverted:before {
372 | border-bottom-color: var(--tooltip-background);
373 | }
374 | .fa-info-circle {
375 | color: var(--main-accent-color) !important;
376 | }
377 | /* End Tooltip */
378 |
379 |
380 | table thead tr th {
381 | color: var(--main-text-color);
382 | }
383 | table thead tr {
384 | border-color: var(--border-color);
385 | }
386 | #locale-select {
387 | width: 50%;
388 | min-width: 10rem;
389 | }
390 | .grecaptcha-msg {
391 | margin: 1rem 0;
392 | text-align: left;
393 | }
394 | .progress-bar {
395 | border-radius: .5rem;
396 | border: 1px solid var(--border-color);
397 | height: 1rem;
398 | }
399 | .progress-bar div {
400 | border-radius: .5rem;
401 | background: var(--main-accent-color);
402 | height: 1rem;
403 | }
404 | hr,
405 | .hr-container hr {
406 | border: none;
407 | height: 1px;
408 | background-color: #979797;
409 | }
410 | .hr-container div {
411 | color: #959595;
412 | font-size: .75rem;
413 | }
414 | .page-body > .row.center:last-of-type {
415 | width: calc(100% - 30px);
416 | margin: auto;
417 | justify-content: space-between;
418 | }
419 | .page-body > .row.center:last-of-type > div {
420 | width: 50%;
421 | margin: 0;
422 | }
423 | @media only screen and (max-width: 450px) {
424 | .page-body > .row.center:last-of-type {
425 | flex-direction: column-reverse;
426 | align-items: center;
427 | }
428 | .secondary-btn, main.page-body .row:last-of-type a {
429 | margin-bottom: 1rem;
430 | }
431 | .page-body > .row.center:last-of-type > div {
432 | text-align: center !important;
433 | }
434 | }
435 | @media only screen and (min-width: 768px) {
436 | .page-body > .row.center:last-of-type {
437 | width: 33rem;
438 | }
439 | }
440 |
441 | /* Overriding existing grid per page */
442 | #oauth-register .page-body > .row > .col-xs,
443 | #oauth-register .page-body > .row > .col-sm-8,
444 | #oauth-register .page-body > .row > .col-md-6,
445 | #oauth-register .page-body > .row > .col-lg-5,
446 | #oauth-register .page-body > .row > .col-xl-4,
447 | #oauth-authorize .page-body > .row > .col-xs,
448 | #oauth-authorize .page-body > .row > .col-sm-8,
449 | #oauth-authorize .page-body > .row > .col-md-6,
450 | #oauth-authorize .page-body > .row > .col-lg-5,
451 | #oauth-authorize .page-body > .row > .col-xl-4,
452 | #oauth-passwordless .page-body > .row > .col-xs,
453 | #oauth-passwordless .page-body > .row > .col-sm-8,
454 | #oauth-passwordless .page-body > .row > .col-md-6,
455 | #oauth-passwordless .page-body > .row > .col-lg-5,
456 | #oauth-passwordless .page-body > .row > .col-xl-4,
457 | #oauth-two-factor .page-body > .row > .col-xs,
458 | #oauth-two-factor .page-body > .row > .col-sm-8,
459 | #oauth-two-factor .page-body > .row > .col-md-6,
460 | #oauth-two-factor .page-body > .row > .col-lg-5,
461 | #oauth-two-factor .page-body > .row > .col-xl-4,
462 | #oauth-two-factor-methods .page-body > .row > .col-xs,
463 | #oauth-two-factor-methods .page-body > .row > .col-sm-8,
464 | #oauth-two-factor-methods .page-body > .row > .col-md-6,
465 | #oauth-two-factor-methods .page-body > .row > .col-lg-5,
466 | #oauth-two-factor-methods .page-body > .row > .col-xl-4,
467 | #oauth-logout .page-body > .row > .col-xs,
468 | #oauth-logout .page-body > .row > .col-sm-8,
469 | #oauth-logout .page-body > .row > .col-md-6,
470 | #oauth-logout .page-body > .row > .col-lg-5,
471 | #oauth-logout .page-body > .row > .col-xl-4,
472 | #oauth-device .page-body > .row > .col-xs,
473 | #oauth-device .page-body > .row > .col-sm-8,
474 | #oauth-device .page-body > .row > .col-md-6,
475 | #oauth-device .page-body > .row > .col-lg-5,
476 | #oauth-device .page-body > .row > .col-xl-4,
477 | #oauth-device-complete .page-body > .row > .col-xs,
478 | #oauth-device-complete .page-body > .row > .col-sm-8,
479 | #oauth-device-complete .page-body > .row > .col-md-6,
480 | #oauth-device-complete .page-body > .row > .col-lg-5,
481 | #oauth-device-complete .page-body > .row > .col-xl-4,
482 | #oauth-complete-reg .page-body > .row > .col-xs,
483 | #oauth-complete-reg .page-body > .row > .col-sm-8,
484 | #oauth-complete-reg .page-body > .row > .col-md-6,
485 | #oauth-complete-reg .page-body > .row > .col-lg-5,
486 | #oauth-complete-reg .page-body > .row > .col-xl-4,
487 | #oauth-child-reg .page-body > .row > .col-xs,
488 | #oauth-child-reg .page-body > .row > .col-sm-8,
489 | #oauth-child-reg .page-body > .row > .col-md-6,
490 | #oauth-child-reg .page-body > .row > .col-lg-5,
491 | #oauth-child-reg .page-body > .row > .col-xl-4,
492 | #oauth-child-reg-complete .page-body > .row > .col-xs,
493 | #oauth-child-reg-complete .page-body > .row > .col-sm-8,
494 | #oauth-child-reg-complete .page-body > .row > .col-md-6,
495 | #oauth-child-reg-complete .page-body > .row > .col-lg-5,
496 | #oauth-child-reg-complete .page-body > .row > .col-xl-4,
497 | #oauth-not-registered .page-body > .row > .col-xs,
498 | #oauth-not-registered .page-body > .row > .col-sm-8,
499 | #oauth-not-registered .page-body > .row > .col-md-6,
500 | #oauth-not-registered .page-body > .row > .col-lg-5,
501 | #oauth-not-registered .page-body > .row > .col-xl-4,
502 | #oauth-error .page-body > .row > .col-xs,
503 | #oauth-error .page-body > .row > .col-sm-8,
504 | #oauth-error .page-body > .row > .col-md-6,
505 | #oauth-error .page-body > .row > .col-lg-5,
506 | #oauth-error .page-body > .row > .col-xl-4,
507 | #oauthstart-idp-link .page-body > .row > .col-xs,
508 | #oauthstart-idp-link .page-body > .row > .col-sm-8,
509 | #oauthstart-idp-link .page-body > .row > .col-md-6,
510 | #oauthstart-idp-link .page-body > .row > .col-lg-5,
511 | #oauthstart-idp-link .page-body > .row > .col-xl-4,
512 | #oauth-wait .page-body > .row > .col-xs,
513 | #oauth-wait .page-body > .row > .col-sm-8,
514 | #oauth-wait .page-body > .row > .col-md-6,
515 | #oauth-wait .page-body > .row > .col-lg-5,
516 | #oauth-wait .page-body > .row > .col-xl-4,
517 | #email-verification .page-body > .row > .col-xs,
518 | #email-verification .page-body > .row > .col-sm-8,
519 | #email-verification .page-body > .row > .col-md-6,
520 | #email-verification .page-body > .row > .col-lg-5,
521 | #email-verification .page-body > .row > .col-xl-4,
522 | #email-ver-required .page-body > .row > .col-xs,
523 | #email-ver-required .page-body > .row > .col-sm-8,
524 | #email-ver-required .page-body > .row > .col-md-6,
525 | #email-ver-required .page-body > .row > .col-lg-5,
526 | #email-ver-required .page-body > .row > .col-xl-4,
527 | #email-ver-complete .page-body > .row > .col-xs,
528 | #email-ver-complete .page-body > .row > .col-sm-8,
529 | #email-ver-complete .page-body > .row > .col-md-6,
530 | #email-ver-complete .page-body > .row > .col-lg-5,
531 | #email-ver-complete .page-body > .row > .col-xl-4,
532 | #email-ver-resent .page-body > .row > .col-xs,
533 | #email-ver-resent .page-body > .row > .col-sm-8,
534 | #email-ver-resent .page-body > .row > .col-md-6,
535 | #email-ver-resent .page-body > .row > .col-lg-5,
536 | #email-ver-resent .page-body > .row > .col-xl-4,
537 | #forgot-pwd .page-body > .row > .col-xs,
538 | #forgot-pwd .page-body > .row > .col-sm-8,
539 | #forgot-pwd .page-body > .row > .col-md-6,
540 | #forgot-pwd .page-body > .row > .col-lg-5,
541 | #forgot-pwd .page-body > .row > .col-xl-4,
542 | #forgot-pwd-sent .page-body > .row > .col-xs,
543 | #forgot-pwd-sent .page-body > .row > .col-sm-8,
544 | #forgot-pwd-sent .page-body > .row > .col-md-6,
545 | #forgot-pwd-sent .page-body > .row > .col-lg-5,
546 | #forgot-pwd-sent .page-body > .row > .col-xl-4,
547 | #verify-reg .page-body > .row > .col-xs,
548 | #verify-reg .page-body > .row > .col-sm-8,
549 | #verify-reg .page-body > .row > .col-md-6,
550 | #verify-reg .page-body > .row > .col-lg-5,
551 | #verify-reg .page-body > .row > .col-xl-4,
552 | #verify-reg-complete .page-body > .row > .col-xs,
553 | #verify-reg-complete .page-body > .row > .col-sm-8,
554 | #verify-reg-complete .page-body > .row > .col-md-6,
555 | #verify-reg-complete .page-body > .row > .col-lg-5,
556 | #verify-reg-complete .page-body > .row > .col-xl-4,
557 | #verify-reg-resent .page-body > .row > .col-xs,
558 | #verify-reg-resent .page-body > .row > .col-sm-8,
559 | #verify-reg-resent .page-body > .row > .col-md-6,
560 | #verify-reg-resent .page-body > .row > .col-lg-5,
561 | #verify-reg-resent .page-body > .row > .col-xl-4,
562 | #verify-reg-required .page-body > .row > .col-xs,
563 | #verify-reg-required .page-body > .row > .col-sm-8,
564 | #verify-reg-required .page-body > .row > .col-md-6,
565 | #verify-reg-required .page-body > .row > .col-lg-5,
566 | #verify-reg-required .page-body > .row > .col-xl-4,
567 | #acct-2fa-enable .page-body > .row > .col-xs-12,
568 | #acct-2fa-enable .page-body > .row > .col-sm-12,
569 | #acct-2fa-enable .page-body > .row > .col-md-10,
570 | #acct-2fa-enable .page-body > .row > .col-lg-8,
571 | #acct-2fa-disable .page-body > .row > .col-xs-12,
572 | #acct-2fa-disable .page-body > .row > .col-sm-12,
573 | #acct-2fa-disable .page-body > .row > .col-md-10,
574 | #acct-2fa-disable .page-body > .row > .col-lg-8,
575 | #unauthorized-page .page-body > .row > .col-sm-10,
576 | #unauthorized-page .page-body > .row > .col-md-8,
577 | #unauthorized-page .page-body > .row > .col-lg-7,
578 | #unauthorized-page .page-body > .row > .col-xl-5,
579 | #change-pwd .page-body > .row > .col-xs,
580 | #change-pwd .page-body > .row > .col-sm-8,
581 | #change-pwd .page-body > .row > .col-md-6,
582 | #change-pwd .page-body > .row > .col-lg-5,
583 | #change-pwd .page-body > .row > .col-xl-4,
584 | #change-pwd-complete .page-body > .row > .col-xs,
585 | #change-pwd-complete .page-body > .row > .col-sm-8,
586 | #change-pwd-complete .page-body > .row > .col-md-6,
587 | #change-pwd-complete .page-body > .row > .col-lg-5,
588 | #change-pwd-complete .page-body > .row > .col-xl-4 {
589 | flex-basis: 33rem;
590 | width: calc(100% - 30px);
591 | max-width: 33rem;
592 | }
593 | @media only screen and (max-width: 575px) {
594 | #oauth-register .page-body > .row > .col-xs,
595 | #oauth-register .page-body > .row > .col-sm-8,
596 | #oauth-register .page-body > .row > .col-md-6,
597 | #oauth-register .page-body > .row > .col-lg-5,
598 | #oauth-register .page-body > .row > .col-xl-4,
599 | #oauth-authorize .page-body > .row > .col-xs,
600 | #oauth-authorize .page-body > .row > .col-sm-8,
601 | #oauth-authorize .page-body > .row > .col-md-6,
602 | #oauth-authorize .page-body > .row > .col-lg-5,
603 | #oauth-authorize .page-body > .row > .col-xl-4,
604 | #oauth-passwordless .page-body > .row > .col-xs,
605 | #oauth-passwordless .page-body > .row > .col-sm-8,
606 | #oauth-passwordless .page-body > .row > .col-md-6,
607 | #oauth-passwordless .page-body > .row > .col-lg-5,
608 | #oauth-passwordless .page-body > .row > .col-xl-4,
609 | #oauth-two-factor .page-body > .row > .col-xs,
610 | #oauth-two-factor .page-body > .row > .col-sm-8,
611 | #oauth-two-factor .page-body > .row > .col-md-6,
612 | #oauth-two-factor .page-body > .row > .col-lg-5,
613 | #oauth-two-factor .page-body > .row > .col-xl-4,
614 | #oauth-two-factor-methods .page-body > .row > .col-xs,
615 | #oauth-two-factor-methods .page-body > .row > .col-sm-8,
616 | #oauth-two-factor-methods .page-body > .row > .col-md-6,
617 | #oauth-two-factor-methods .page-body > .row > .col-lg-5,
618 | #oauth-two-factor-methods .page-body > .row > .col-xl-4,
619 | #oauth-logout .page-body > .row > .col-xs,
620 | #oauth-logout .page-body > .row > .col-sm-8,
621 | #oauth-logout .page-body > .row > .col-md-6,
622 | #oauth-logout .page-body > .row > .col-lg-5,
623 | #oauth-logout .page-body > .row > .col-xl-4,
624 | #oauth-device .page-body > .row > .col-xs,
625 | #oauth-device .page-body > .row > .col-sm-8,
626 | #oauth-device .page-body > .row > .col-md-6,
627 | #oauth-device .page-body > .row > .col-lg-5,
628 | #oauth-device .page-body > .row > .col-xl-4,
629 | #oauth-device-complete .page-body > .row > .col-xs,
630 | #oauth-device-complete .page-body > .row > .col-sm-8,
631 | #oauth-device-complete .page-body > .row > .col-md-6,
632 | #oauth-device-complete .page-body > .row > .col-lg-5,
633 | #oauth-device-complete .page-body > .row > .col-xl-4,
634 | #oauth-complete-reg .page-body > .row > .col-xs,
635 | #oauth-complete-reg .page-body > .row > .col-sm-8,
636 | #oauth-complete-reg .page-body > .row > .col-md-6,
637 | #oauth-complete-reg .page-body > .row > .col-lg-5,
638 | #oauth-complete-reg .page-body > .row > .col-xl-4,
639 | #oauth-child-reg .page-body > .row > .col-xs,
640 | #oauth-child-reg .page-body > .row > .col-sm-8,
641 | #oauth-child-reg .page-body > .row > .col-md-6,
642 | #oauth-child-reg .page-body > .row > .col-lg-5,
643 | #oauth-child-reg .page-body > .row > .col-xl-4,
644 | #oauth-child-reg-complete .page-body > .row > .col-xs,
645 | #oauth-child-reg-complete .page-body > .row > .col-sm-8,
646 | #oauth-child-reg-complete .page-body > .row > .col-md-6,
647 | #oauth-child-reg-complete .page-body > .row > .col-lg-5,
648 | #oauth-child-reg-complete .page-body > .row > .col-xl-4,
649 | #oauth-not-registered .page-body > .row > .col-xs,
650 | #oauth-not-registered .page-body > .row > .col-sm-8,
651 | #oauth-not-registered .page-body > .row > .col-md-6,
652 | #oauth-not-registered .page-body > .row > .col-lg-5,
653 | #oauth-not-registered .page-body > .row > .col-xl-4,
654 | #oauth-error .page-body > .row > .col-xs,
655 | #oauth-error .page-body > .row > .col-sm-8,
656 | #oauth-error .page-body > .row > .col-md-6,
657 | #oauth-error .page-body > .row > .col-lg-5,
658 | #oauth-error .page-body > .row > .col-xl-4,
659 | #oauthstart-idp-link .page-body > .row > .col-xs,
660 | #oauthstart-idp-link .page-body > .row > .col-sm-8,
661 | #oauthstart-idp-link .page-body > .row > .col-md-6,
662 | #oauthstart-idp-link .page-body > .row > .col-lg-5,
663 | #oauthstart-idp-link .page-body > .row > .col-xl-4,
664 | #oauth-wait .page-body > .row > .col-xs,
665 | #oauth-wait .page-body > .row > .col-sm-8,
666 | #oauth-wait .page-body > .row > .col-md-6,
667 | #oauth-wait .page-body > .row > .col-lg-5,
668 | #oauth-wait .page-body > .row > .col-xl-4,
669 | #email-verification .page-body > .row > .col-xs,
670 | #email-verification .page-body > .row > .col-sm-8,
671 | #email-verification .page-body > .row > .col-md-6,
672 | #email-verification .page-body > .row > .col-lg-5,
673 | #email-verification .page-body > .row > .col-xl-4,
674 | #email-ver-required .page-body > .row > .col-xs,
675 | #email-ver-required .page-body > .row > .col-sm-8,
676 | #email-ver-required .page-body > .row > .col-md-6,
677 | #email-ver-required .page-body > .row > .col-lg-5,
678 | #email-ver-required .page-body > .row > .col-xl-4,
679 | #email-ver-complete .page-body > .row > .col-xs,
680 | #email-ver-complete .page-body > .row > .col-sm-8,
681 | #email-ver-complete .page-body > .row > .col-md-6,
682 | #email-ver-complete .page-body > .row > .col-lg-5,
683 | #email-ver-complete .page-body > .row > .col-xl-4,
684 | #email-ver-resent .page-body > .row > .col-xs,
685 | #email-ver-resent .page-body > .row > .col-sm-8,
686 | #email-ver-resent .page-body > .row > .col-md-6,
687 | #email-ver-resent .page-body > .row > .col-lg-5,
688 | #email-ver-resent .page-body > .row > .col-xl-4,
689 | #forgot-pwd .page-body > .row > .col-xs,
690 | #forgot-pwd .page-body > .row > .col-sm-8,
691 | #forgot-pwd .page-body > .row > .col-md-6,
692 | #forgot-pwd .page-body > .row > .col-lg-5,
693 | #forgot-pwd .page-body > .row > .col-xl-4,
694 | #forgot-pwd-sent .page-body > .row > .col-xs,
695 | #forgot-pwd-sent .page-body > .row > .col-sm-8,
696 | #forgot-pwd-sent .page-body > .row > .col-md-6,
697 | #forgot-pwd-sent .page-body > .row > .col-lg-5,
698 | #forgot-pwd-sent .page-body > .row > .col-xl-4,
699 | #verify-reg .page-body > .row > .col-xs,
700 | #verify-reg .page-body > .row > .col-sm-8,
701 | #verify-reg .page-body > .row > .col-md-6,
702 | #verify-reg .page-body > .row > .col-lg-5,
703 | #verify-reg .page-body > .row > .col-xl-4,
704 | #verify-reg-complete .page-body > .row > .col-xs,
705 | #verify-reg-complete .page-body > .row > .col-sm-8,
706 | #verify-reg-complete .page-body > .row > .col-md-6,
707 | #verify-reg-complete .page-body > .row > .col-lg-5,
708 | #verify-reg-complete .page-body > .row > .col-xl-4,
709 | #verify-reg-resent .page-body > .row > .col-xs,
710 | #verify-reg-resent .page-body > .row > .col-sm-8,
711 | #verify-reg-resent .page-body > .row > .col-md-6,
712 | #verify-reg-resent .page-body > .row > .col-lg-5,
713 | #verify-reg-resent .page-body > .row > .col-xl-4,
714 | #verify-reg-required .page-body > .row > .col-xs,
715 | #verify-reg-required .page-body > .row > .col-sm-8,
716 | #verify-reg-required .page-body > .row > .col-md-6,
717 | #verify-reg-required .page-body > .row > .col-lg-5,
718 | #verify-reg-required .page-body > .row > .col-xl-4,
719 | #acct-2fa-enable .page-body > .row > .col-xs-12,
720 | #acct-2fa-enable .page-body > .row > .col-sm-12,
721 | #acct-2fa-enable .page-body > .row > .col-md-10,
722 | #acct-2fa-enable .page-body > .row > .col-lg-8,
723 | #acct-2fa-disable .page-body > .row > .col-xs-12,
724 | #acct-2fa-disable .page-body > .row > .col-sm-12,
725 | #acct-2fa-disable .page-body > .row > .col-md-10,
726 | #acct-2fa-disable .page-body > .row > .col-lg-8,
727 | #unauthorized-page .page-body > .row > .col-sm-10,
728 | #unauthorized-page .page-body > .row > .col-md-8,
729 | #unauthorized-page .page-body > .row > .col-lg-7,
730 | #unauthorized-page .page-body > .row > .col-xl-5,
731 | #change-pwd .page-body > .row > .col-xs,
732 | #change-pwd .page-body > .row > .col-sm-8,
733 | #change-pwd .page-body > .row > .col-md-6,
734 | #change-pwd .page-body > .row > .col-lg-5,
735 | #change-pwd .page-body > .row > .col-xl-4,
736 | #change-pwd-complete .page-body > .row > .col-xs,
737 | #change-pwd-complete .page-body > .row > .col-sm-8,
738 | #change-pwd-complete .page-body > .row > .col-md-6,
739 | #change-pwd-complete .page-body > .row > .col-lg-5,
740 | #change-pwd-complete .page-body > .row > .col-xl-4 {
741 | flex-basis: calc(100% - 30px);
742 | width: calc(100% - 30px);
743 | max-width: 33rem;
744 | }
745 | .panel {
746 | padding-left: .5rem;
747 | padding-right: .5rem;
748 | }
749 | }
750 | @media only screen and (min-width: 768px) {
751 | #acct-2fa-index .page-body > .row.center:last-of-type {
752 | width: calc(83.33333333% - 30px);
753 | }
754 | }
755 | @media only screen and (min-width: 992px) {
756 | #acct-2fa-index .page-body > .row > .col-xs12,
757 | #acct-2fa-index .page-body > .row > .col-sm-12,
758 | #acct-2fa-index .page-body > .row > .col-md-10,
759 | #acct-2fa-index .page-body > .row > .col-lg-8 {
760 | flex-basis: 54.125rem;
761 | max-width: 54.125rem;
762 | }
763 | #acct-2fa-index .page-body > .row.center:last-of-type {
764 | width: 54.125rem;
765 | }
766 | }
767 | /* End grid override */
768 |
769 |
770 | /* Cleaning up spacing */
771 | #verify-reg-required .link.blue-text,
772 | #verify-reg-required .grecaptcha-msg,
773 | #verify-reg-required .panel > main > .full fieldset,
774 | #verify-reg .grecaptcha-msg,
775 | #verify-reg .panel > main > .full fieldset,
776 | #email-ver-required .grecaptcha-msg,
777 | #email-verification .grecaptcha-msg,
778 | #email-ver-required fieldset,
779 | #email-ver-required .panel > main > #verification-required-resend-code fieldset,
780 | #oauth-two-factor .panel .full > fieldset,
781 | #oauth-two-factor .panel > main > fieldset + .form-row,
782 | #oauth-two-factor-methods .full,
783 | #oauth-two-factor-methods .blue.button,
784 | #oauth-authorize .panel > main > form > .form-row:first-of-type,
785 | #oauth-passwordless .panel > main > .full > .form-row:first-of-type,
786 | #oauth-register .panel > main > .full > .form-row:first-of-type,
787 | #forgot-pwd .panel > main > .full fieldset,
788 | #forgot-pwd .panel .grecaptcha-msg,
789 | #change-pwd .panel > main .full > .form-row:first-of-type,
790 | #acct-2fa-index .panel > main > fieldset {
791 | margin-bottom: 0;
792 | }
793 | /* End spacing */
794 |
795 |
796 | /* Other page specific styles */
797 |
798 | #acct-2fa-index .blue.button {
799 | max-width: 25rem;
800 | margin-left: auto;
801 | margin-right: auto;
802 | display: block;
803 | }
804 | #acct-2fa-index table {
805 | margin-bottom: 3rem;
806 | }
807 | #acct-2fa-enable .d-flex {
808 | display: block;
809 | }
810 | #acct-2fa-enable #qrcode {
811 | padding-left: 0;
812 | }
813 | #acct-2fa-enable #qrcode img {
814 | margin-left: auto;
815 | margin-right: auto;
816 | }
817 | #acct-2fa-disable main > fieldset {
818 | margin: 0;
819 | }
820 | #oauth-two-factor .panel form > .form-row:last-of-type a .fa {
821 | display: none; /* hiding icon in button */
822 | }
823 | #oauth-two-factor .panel > main > fieldset .form-row.mt-4 {
824 | margin-top: 0;
825 | }
826 | #oauth-two-factor-methods input[type="radio"] {
827 | vertical-align: text-top;
828 | }
829 | #oauth-two-factor-methods .full fieldset {
830 | margin-top: 2rem;
831 | margin-bottom: 0;
832 | }
833 | #oauth-two-factor-methods .full fieldset .form-row:last-child label {
834 | padding-bottom: 0;
835 | }
836 | #oauth-two-factor-methods .radio-items .form-row label span:last-of-type {
837 | margin-left: 1.875rem;
838 | }
839 | #oauth-device .push-top {
840 | margin-top: 0;
841 | }
842 | #oauth-device #device-form > p {
843 | text-align: center;
844 | }
845 | #oauth-device #user_code_container input[type="text"] {
846 | color: var(--main-text-color);
847 | }
848 | #index-page ul li a {
849 | font-family: var(--font-stack);
850 | }
851 | #oauth-passwordless .panel form .form-row:last-of-type p,
852 | #oauth-register .panel form .form-row:last-of-type p,
853 | #oauth-two-factor .panel form > .form-row:last-of-type,
854 | #forgot-pwd .panel form > .form-row:last-of-type p,
855 | #forgot-pwd-sent .panel main p:last-of-type,
856 | #oauth-wait .panel main p:last-of-type {
857 | margin-bottom: 0;
858 | text-align: center;
859 | }
860 |
861 | /* Account Index page */
862 | #acct-index .panel > main {
863 | padding: 0;
864 | }
865 | #acct-index .user-details.mb-5 {
866 | margin-bottom: 0;
867 | }
868 | #acct-index #edit-profile span {
869 | font-size: inherit !important;
870 | }
871 | #acct-index #edit-profile span:after {
872 | content: 'Edit';
873 | margin-left: .25em;
874 | }
875 | #acct-index .user-details > div {
876 | margin: 0;
877 | width: 100%;
878 | max-width: 100%;
879 | flex-basis: 100%;
880 | }
881 | #acct-index .user-details dl {
882 | display: flex;
883 | align-items: flex-start;
884 | justify-content: space-between;
885 | margin: 1.25rem 0;
886 | }
887 | #acct-index .user-details dt {
888 | float: none;
889 | font-weight: 500;
890 | width: 40%;
891 | margin: 0;
892 | }
893 | #acct-index .user-details dd {
894 | width: 60%;
895 | margin: 0;
896 | }
897 | #acct-index .panel {
898 | padding-left: 1.5rem;
899 | padding-right: 1.5rem;
900 | }
901 | #acct-index .panel:before {
902 | content: '';
903 | display: block;
904 | position: absolute;
905 | top: 0;
906 | left: 0;
907 | width: 100%;
908 | height: 4rem;
909 | background-color: var(--main-accent-color);
910 | border-radius: .625rem .625rem 0 0;
911 | }
912 | #acct-index .user-details .avatar > div:last-of-type {
913 | color: var(--main-accent-color);
914 | font-weight: 500;
915 | font-size: 1.5rem;
916 | }
917 | #acct-index .user-details .avatar {
918 | top: -2.25rem;
919 | position: relative;
920 | z-index: 2;
921 | padding: 0;
922 | }
923 | #acct-index .user-details .avatar > div:first-of-type {
924 | max-width: 7.5rem;
925 | padding: 0;
926 | }
927 | #acct-index .user-details .avatar > div:first-of-type img {
928 | border: .625rem solid #ffffff;
929 | }
930 | #acct-index .user-details > div:nth-of-type(2) > div {
931 | padding: 0;
932 | }
933 | #acct-index .user-details > div:nth-of-type(2) > div > div {
934 | margin: 0;
935 | flex-basis: 100%;
936 | width: 100%;
937 | max-width: 100%;
938 | }
939 | #acct-index .user-details .panel-actions {
940 | top: 3.5rem;
941 | right: .25rem;
942 | }
943 | @media only screen and (max-width: 450px) {
944 | #acct-index .user-details dl {
945 | flex-direction: column;
946 | }
947 | #acct-index .user-details dt,
948 | #acct-index .user-details dd {
949 | width: 100%;
950 | margin-bottom: .5em;
951 | }
952 | }
953 | @media only screen and (min-width: 768px) {
954 | #acct-index .panel:before {
955 | width: 4rem;
956 | height: 100%;
957 | border-radius: .625rem 0 0 .625rem;
958 | }
959 | #acct-index .user-details {
960 | display: block;
961 | margin-left: 8rem;
962 | }
963 | #acct-index .user-details > div {
964 | margin: 0;
965 | width: 80%;
966 | max-width: 80%;
967 | }
968 | #acct-index .user-details .avatar {
969 | position: static;
970 | }
971 | #acct-index .user-details .avatar > div:first-of-type {
972 | position: absolute;
973 | left: .75rem;
974 | top: calc(50% - 3.25rem);
975 | width: 6.5rem;
976 | }
977 | #acct-index .user-details .avatar > div:first-of-type img {
978 | border-width: .5rem;
979 | }
980 | #acct-index .user-details .avatar > div:last-of-type {
981 | text-align: left;
982 | }
983 | #acct-index .user-details .panel-actions {
984 | top: -0.25rem;
985 | right: .5rem;
986 | }
987 | #acct-index .page-body > .row.center:last-of-type {
988 | width: calc(83.33333333% - 30px);
989 | }
990 | }
991 | @media only screen and (min-width: 992px) {
992 | #acct-index .page-body > .row:first-of-type > .col-xs-12,
993 | #acct-index .page-body > .row:first-of-type > .col-sm-12,
994 | #acct-index .page-body > .row:first-of-type > .col-md-10,
995 | #acct-index .page-body > .row:first-of-type > .col-lg-8 {
996 | flex-basis: 54.125rem;
997 | max-width: 54.125rem;
998 | }
999 | #acct-index .page-body > .row.center:last-of-type {
1000 | width: 54.125rem;
1001 | }
1002 | #acct-index .panel:before {
1003 | width: 6.25rem;
1004 | }
1005 | #acct-index .user-details {
1006 | margin-left: 13rem;
1007 | }
1008 | #acct-index .user-details .panel-actions {
1009 | padding: 0;
1010 | top: 2.25rem;
1011 | right: 2.25rem;
1012 | }
1013 | #acct-index .user-details .panel-actions .status,
1014 | #acct-index .user-details .panel-actions #edit-profile {
1015 | margin: 0;
1016 | }
1017 | #acct-index .user-details > div:first-of-type {
1018 | border: none;
1019 | width: auto;
1020 | flex-basis: auto;
1021 | }
1022 | #acct-index .user-details .avatar {
1023 | left: -.25rem;
1024 | }
1025 | #acct-index .user-details .avatar > div:first-of-type {
1026 | width: 8.75rem;
1027 | max-width: 8.75rem;
1028 | top: calc(50% - 4.5rem);
1029 | left: 1.875rem;
1030 | }
1031 | #acct-index .user-details .avatar > div:first-of-type img {
1032 | border-width: .75rem;
1033 | }
1034 | }
1035 | /*End Account Index page */
1036 |
1037 |
1038 | /* specific page button/link overrides */
1039 | #acct-2fa-enable .gray.button {
1040 | color: var(--main-accent-color) !important;
1041 | padding: .5em .75em !important;
1042 | border: 1px solid var(--main-accent-color) !important;
1043 | border-radius: .25em;
1044 | font-size: .75rem !important;
1045 | margin: 1rem auto;
1046 | display: block;
1047 | line-height: normal !important;
1048 | background: transparent !important;
1049 | }
1050 | #email-ver-required .link.blue-text {
1051 | display: block;
1052 | margin: 1rem auto;
1053 | text-decoration: underline;
1054 | }
1055 | #oauth-two-factor .panel form > .form-row:last-of-type a,
1056 | #verify-reg-required .panel .link.blue-text {
1057 | display: block;
1058 | margin: 1rem auto 0 auto;
1059 | text-decoration: underline;
1060 | }
1061 | #email-ver-required .link.blue-text .fa,
1062 | #verify-reg-required .panel .link.blue-text .fa {
1063 | display: none; /* hiding icon in link */
1064 | }
1065 | #oauth-passwordless .panel form .form-row:last-of-type a,
1066 | #oauth-register .panel form .form-row:last-of-type a,
1067 | #forgot-pwd .panel form > .form-row:last-of-type a,
1068 | #forgot-pwd-sent .panel main p:last-of-type a,
1069 | #oauth-wait .panel main p:last-of-type a {
1070 | color: var(--main-accent-color) !important;
1071 | padding: .5em .75em;
1072 | border: 1px solid var(--main-accent-color) !important;
1073 | border-radius: .25em;
1074 | font-size: .75rem;
1075 | margin: 1rem auto 0 auto;
1076 | display: inline-block;
1077 | line-height: normal;
1078 | text-decoration: none;
1079 | }
1080 | #forgot-pwd-sent .panel main p:last-of-type a {
1081 | color: var(--main-accent-color) !important;
1082 | padding: .5em .75em;
1083 | border: 1px solid var(--main-accent-color) !important;
1084 | border-radius: .25em;
1085 | font-size: .75rem;
1086 | margin: 0 auto;
1087 | display: inline-block;
1088 | line-height: normal;
1089 | text-decoration: none;
1090 | }
1091 | #oauthstart-idp-link .blue.button {
1092 | height: auto !important;
1093 | margin-top: 0;
1094 | }
1095 | #oauthstart-idp-link .panel main div:last-of-type a {
1096 | display: block;
1097 | border: none;
1098 | margin-top: 0;
1099 | padding: 0;
1100 | }
1101 | /* End page specific buttons and links */
1102 |
1103 |
--------------------------------------------------------------------------------