├── docs ├── Ngrok.png ├── Facebook4.png ├── Facebook5.png ├── Facebook6.png ├── Facebook7.png ├── CreateApp1.png ├── CreateApp2.png ├── FacebookLogin2.png ├── FacebookLogin3.png ├── HotSpotMapper.png ├── HotSpotMapper2.png ├── KeycloakRadius.png ├── SelectFacbook.png ├── hotspotClient.png ├── hotspotRealm.png ├── HotSpotMapper2_1.png ├── KeycloakHostName.png ├── KeycloakRadius2.png ├── addWalledGarden.png ├── Copy Redirect URI.png ├── RadiusClientHotSpot.png ├── FacebookLoginHotspot.png ├── FacebookWalledGarden.png ├── downloadKeycloakJson.png └── HotspotClientConfiguration.png ├── source ├── jest.config.js ├── unitTestConfig │ └── jestSetup.js ├── .npmignore ├── babel.config.js ├── src │ ├── base │ │ ├── restCalls.js │ │ ├── loginUtils.js │ │ └── keyCloakCerts.js │ └── index.js ├── dev │ ├── indexDev.js │ └── login.html ├── package.json ├── webpack.config.babel.js ├── prod │ └── login.html └── README.md ├── mikrotik ├── authorization.js.LICENSE.txt ├── index.js └── login.html ├── .circleci └── config.yml ├── .gitignore ├── README.md └── LICENSE /docs/Ngrok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Ngrok.png -------------------------------------------------------------------------------- /docs/Facebook4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Facebook4.png -------------------------------------------------------------------------------- /docs/Facebook5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Facebook5.png -------------------------------------------------------------------------------- /docs/Facebook6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Facebook6.png -------------------------------------------------------------------------------- /docs/Facebook7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Facebook7.png -------------------------------------------------------------------------------- /docs/CreateApp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/CreateApp1.png -------------------------------------------------------------------------------- /docs/CreateApp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/CreateApp2.png -------------------------------------------------------------------------------- /docs/FacebookLogin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/FacebookLogin2.png -------------------------------------------------------------------------------- /docs/FacebookLogin3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/FacebookLogin3.png -------------------------------------------------------------------------------- /docs/HotSpotMapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/HotSpotMapper.png -------------------------------------------------------------------------------- /docs/HotSpotMapper2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/HotSpotMapper2.png -------------------------------------------------------------------------------- /docs/KeycloakRadius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/KeycloakRadius.png -------------------------------------------------------------------------------- /docs/SelectFacbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/SelectFacbook.png -------------------------------------------------------------------------------- /docs/hotspotClient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/hotspotClient.png -------------------------------------------------------------------------------- /docs/hotspotRealm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/hotspotRealm.png -------------------------------------------------------------------------------- /docs/HotSpotMapper2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/HotSpotMapper2_1.png -------------------------------------------------------------------------------- /docs/KeycloakHostName.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/KeycloakHostName.png -------------------------------------------------------------------------------- /docs/KeycloakRadius2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/KeycloakRadius2.png -------------------------------------------------------------------------------- /docs/addWalledGarden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/addWalledGarden.png -------------------------------------------------------------------------------- /docs/Copy Redirect URI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/Copy Redirect URI.png -------------------------------------------------------------------------------- /docs/RadiusClientHotSpot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/RadiusClientHotSpot.png -------------------------------------------------------------------------------- /docs/FacebookLoginHotspot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/FacebookLoginHotspot.png -------------------------------------------------------------------------------- /docs/FacebookWalledGarden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/FacebookWalledGarden.png -------------------------------------------------------------------------------- /docs/downloadKeycloakJson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/downloadKeycloakJson.png -------------------------------------------------------------------------------- /docs/HotspotClientConfiguration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzakharchenko/mikrotik-hotspot-oauth/HEAD/docs/HotspotClientConfiguration.png -------------------------------------------------------------------------------- /source/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ['./unitTestConfig/jestSetup.js'], 3 | transform: { 4 | '^.+\\.js$': 'babel-jest', 5 | }, 6 | transformIgnorePatterns: ['/node_modules/'], 7 | }; 8 | -------------------------------------------------------------------------------- /source/unitTestConfig/jestSetup.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | 3 | import { shallow, mount, render } from 'enzyme'; 4 | 5 | /** 6 | * enzyme settings for all tests 7 | */ 8 | 9 | global.shallow = shallow; 10 | global.mount = mount; 11 | global.render = render; 12 | -------------------------------------------------------------------------------- /mikrotik/authorization.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 9 | 10 | /*! safe-buffer. MIT License. Feross Aboukhadijeh */ 11 | 12 | /** 13 | * [js-sha256]{@link https://github.com/emn178/js-sha256} 14 | * 15 | * @version 0.9.0 16 | * @author Chen, Yi-Cyuan [emn178@gmail.com] 17 | * @copyright Chen, Yi-Cyuan 2014-2017 18 | * @license MIT 19 | */ 20 | -------------------------------------------------------------------------------- /source/.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | .idea 27 | package-lock.json 28 | .settings 29 | .sign 30 | userChannels.json 31 | .currentChannel 32 | keycloak.json 33 | yarn.lock 34 | yarn-error.log 35 | *.tgz 36 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: 3 | build: 4 | working_directory: ~/mikrotik-hotspot-oauth # directory where steps will run 5 | 6 | docker: # run the steps with Docker 7 | - image: circleci/openjdk:11.0.2-node 8 | 9 | steps: # a collection of executable commands 10 | 11 | - checkout # check out source code to working directory 12 | 13 | - run: 14 | name: install hotspot ui 15 | command: cd source && npm i 16 | - run: 17 | name: build hotspot ui 18 | command: cd source && npm run lint 19 | - run: 20 | name: build hotspot ui 21 | command: cd source && npm run build 22 | -------------------------------------------------------------------------------- /source/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | '@babel/env', 6 | ]; 7 | const plugins = [ 8 | [ 9 | '@babel/transform-runtime', 10 | { 11 | regenerator: true, 12 | }, 13 | ], 14 | [ 15 | '@babel/plugin-proposal-decorators', 16 | { 17 | legacy: true, 18 | }, 19 | ], 20 | ]; 21 | 22 | // if (process.env.NODE_ENV === 'test') { 23 | // plugins.push('babel-plugin-dynamic-import-node'); 24 | // } else { 25 | // plugins.push('@babel/plugin-syntax-dynamic-import'); 26 | // } 27 | 28 | return { 29 | presets, 30 | plugins, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /source/src/base/restCalls.js: -------------------------------------------------------------------------------- 1 | import fetch from 'axios'; 2 | 3 | function errorHandler(response) { 4 | console.debug('error:', response.data); 5 | } 6 | 7 | export function fetchData(url, method = 'GET', headers) { 8 | return new Promise((resolve, reject) => { 9 | fetch({ 10 | url, 11 | method, 12 | headers, 13 | transformResponse: req => req, 14 | withCredentials: true, 15 | }).then((response) => { 16 | resolve(response); 17 | }).catch((response) => { 18 | errorHandler(response); 19 | reject(response); 20 | }); 21 | }); 22 | } 23 | 24 | export function sendData(url, method = 'POST', data, headers) { 25 | return new Promise((resolve, reject) => { 26 | fetch({ 27 | url, 28 | method, 29 | data, 30 | transformResponse: req => req, 31 | headers, 32 | withCredentials: true, 33 | }).then((response) => { 34 | resolve(response); 35 | }).catch((response) => { 36 | errorHandler(response); 37 | reject(response); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /mikrotik/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars,no-var,vars-on-top */ 2 | // Common server variables 3 | var hostname = '$(hostname)'; 4 | var identity = '$(identity)'; 5 | var serverAddress = '$(server-address)'; 6 | var sslLogin = '$(ssl-login)'; 7 | var serverName = '$(server-name)'; 8 | 9 | // Links 10 | var linkLogin = '$(link-login)'; 11 | var loginOnly = '$(link-login-only)'; 12 | var linkLogout = '$(link-logout)'; 13 | var linkStatus = '$(link-status)'; 14 | var linkOrig = '$(link-orig)'; 15 | 16 | var chapId = '$(chap-id)'; 17 | var chapChallenge = '$(chap-challenge)'; 18 | var error = ''; 19 | var trial = '$(trial)'; 20 | 21 | // eslint-disable-next-line no-unused-vars 22 | function doLogin(username, password) { 23 | document.sendin.action = loginOnly; 24 | document.sendin.username.value = username; 25 | document.sendin.dst.value = linkOrig; 26 | var psw = password; 27 | if (chapId) { 28 | psw = hexMD5(chapId + password + chapChallenge); 29 | } 30 | document.sendin.password.value = psw; 31 | document.sendin.submit(); 32 | return false; 33 | } 34 | -------------------------------------------------------------------------------- /source/dev/indexDev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars,no-var,vars-on-top */ 2 | // Common server variables 3 | var hostname = '$(hostname)'; 4 | var identity = '$(identity)'; 5 | var serverAddress = '$(server-address)'; 6 | var sslLogin = '$(ssl-login)'; 7 | var serverName = '$(server-name)'; 8 | 9 | // Links 10 | var linkLogin = '$(link-login)'; 11 | var loginOnly = '$(link-login-only)'; 12 | var linkLogout = '$(link-logout)'; 13 | var linkStatus = '$(link-status)'; 14 | var linkOrig = '$(link-orig)'; 15 | 16 | var chapId = '$(chap-id)'; 17 | var chapChallenge = '$(chap-challenge)'; 18 | var error = ''; 19 | var trial = '$(trial)'; 20 | 21 | // eslint-disable-next-line no-unused-vars 22 | function doLogin(username, password) { 23 | document.sendin.action = loginOnly; 24 | document.sendin.username.value = username; 25 | document.sendin.dst.value = linkOrig; 26 | var psw = password; 27 | if (chapId) { 28 | psw = hexMD5(chapId + password + chapChallenge); 29 | } 30 | document.sendin.password.value = psw; 31 | document.sendin.submit(); 32 | return false; 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.war 15 | *.nar 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | .DS_STORE 25 | 26 | node_modules 27 | package-lock.json 28 | keycloak.json 29 | .env.json 30 | storage.json 31 | .idea/ 32 | target/ 33 | public 34 | 35 | .settings 36 | .metadata 37 | 38 | # temp/backup files # 39 | ##################### 40 | *~ 41 | *.tmp 42 | .*.swp 43 | junk 44 | vimsession 45 | tags 46 | tmp/ 47 | */tmp/* 48 | 49 | # compiled source # 50 | ################### 51 | *.swf 52 | *.swc 53 | *.com 54 | *.dll 55 | *.exe 56 | *.o 57 | *.so 58 | 59 | # Packages # 60 | ############ 61 | # it's better to unpack these files and commit the raw source 62 | # git has its own built in compression methods 63 | *.7z 64 | *.dmg 65 | *.gz 66 | *.iso 67 | *.rar 68 | *.tar 69 | 70 | *.zip 71 | 72 | # Logs and databases # 73 | ###################### 74 | #*.sql 75 | *.sqliteqq 76 | *.trc 77 | 78 | # OS generated files # 79 | ###################### 80 | .DS_Store 81 | Thumbs.db 82 | 83 | *.iml 84 | *.idea 85 | .env 86 | keycloak.properties 87 | keycloak.json 88 | 89 | -------------------------------------------------------------------------------- /source/src/index.js: -------------------------------------------------------------------------------- 1 | import Keycloak from 'keycloak-js'; 2 | import { 3 | getPassword, getUserName, loadKeycloakJson, verifyToken, 4 | } from './base/loginUtils'; 5 | 6 | 7 | function setError(message) { 8 | const errorElement = document.getElementById('error'); 9 | errorElement.innerHTML = `
${message}
`; 10 | } 11 | 12 | function clear() { 13 | const rootElement = document.getElementById('root'); 14 | rootElement.innerHTML = 'Please wait...'; 15 | } 16 | if (!error) { 17 | loadKeycloakJson() 18 | .then((json) => { 19 | const keycloak = new Keycloak('/keycloak.json'); 20 | keycloak.init({ 21 | onLoad: 'login-required', 22 | promiseType: 'native', 23 | }).then((authenticated) => { 24 | if (authenticated) { 25 | verifyToken(keycloak.token, json).then((decodedToken) => { 26 | const password = getPassword(decodedToken); 27 | const userName = getUserName(decodedToken); 28 | if (!userName || !password) { 29 | setError(`Client ${json.resource} is not configured. Please check client mapper.`); 30 | } else { 31 | doLogin(userName, password); 32 | clear(); 33 | } 34 | }).catch((ve) => { 35 | setError(`failed to verify token, please check configuration: ${ve}`); 36 | }); 37 | } else { 38 | setError('failed to initialize SSO, please check configuration'); 39 | } 40 | }).catch((e) => { 41 | setError(`failed to initialize SSO, please check configuration: ${e}`); 42 | }); 43 | }) 44 | .catch((e) => { 45 | setError(`failed to load keycloak.json, please check configuration: ${e}`); 46 | }); 47 | } else { 48 | setError(`${error}`); 49 | } 50 | -------------------------------------------------------------------------------- /source/src/base/loginUtils.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { KeycloakPublicKeyFetcher } from './keyCloakCerts'; 3 | import { fetchData } from './restCalls'; 4 | 5 | function getRealmName(url) { 6 | const n = url.lastIndexOf('/'); 7 | return url.substring(n + 1); 8 | } 9 | 10 | function getRealmNameFromToken(payloadjwt) { 11 | return getRealmName(payloadjwt.iss); 12 | } 13 | 14 | export async function verifyToken(token, keycloakJSON) { 15 | const decodedJwt = jwt.decode(token, { complete: true }); 16 | if (!decodedJwt || !decodedJwt.header) { 17 | throw new Error('invalid token (header part)'); 18 | } else { 19 | const { kid } = decodedJwt.header; 20 | const { alg } = decodedJwt.header; 21 | const realm = getRealmNameFromToken(decodedJwt.payload); 22 | if (alg.toLowerCase() !== 'none' && !alg.toLowerCase().startsWith('hs') && kid) { 23 | // fetch the PEM Public Key 24 | 25 | try { 26 | const publicKeyFunc = KeycloakPublicKeyFetcher(keycloakJSON['auth-server-url'], 27 | realm, 28 | kid); 29 | const key = await publicKeyFunc; 30 | return jwt.verify(token, key); 31 | } catch (e) { 32 | // Token is not valid 33 | throw new Error(`invalid token: ${e}`); 34 | } 35 | } else { 36 | throw new Error('invalid token'); 37 | } 38 | } 39 | } 40 | 41 | export function getPassword(decodedToken) { 42 | return decodedToken[decodedToken.np || 'p']; 43 | } 44 | 45 | export function getUserName(decodedToken) { 46 | return decodedToken[decodedToken.n]; 47 | } 48 | 49 | export function loadKeycloakJson() { 50 | return new Promise((resolve, reject) => { 51 | fetchData('/keycloak.json').then((r) => { 52 | resolve(JSON.parse(r.data)); 53 | }).catch((e) => { 54 | if (e.response && e.response.status === 404) { 55 | // eslint-disable-next-line prefer-promise-reject-errors 56 | reject('Cannot found /keycloak.json'); 57 | } else { 58 | reject(e.response ? e.response.data : e); 59 | } 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotspot-login-page", 3 | "version": "1.0.1", 4 | "description": "hotspot login page to work with keycloak", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=development webpack-dev-server --hot --mode=development", 8 | "build": "NODE_ENV=production webpack --bail --config webpack.config.babel.js", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "eslint --quiet --ext .js src", 11 | "lint:fix": "eslint --fix --quiet --ext .js src" 12 | }, 13 | "author": "vzakharchenko", 14 | "license": "ISC", 15 | "keywords": [ 16 | "hotspot-wifi", 17 | "hotspot", 18 | "mikrotik-hotspot", 19 | "mikrotik", 20 | "radius-server", 21 | "keycloak", 22 | "openid", 23 | "saml2", 24 | "facebook", 25 | "google", 26 | "Idp"], 27 | "devDependencies": { 28 | "@babel/core": "^7.7.7", 29 | "@babel/plugin-proposal-decorators": "^7.7.4", 30 | "@babel/plugin-transform-runtime": "^7.7.6", 31 | "@babel/preset-env": "^7.2.0", 32 | "@babel/preset-stage-3": "^7.0.0", 33 | "babel-eslint": "^10.0.1", 34 | "babel-loader": "^8.0.4", 35 | "babel-preset-es2015": "^6.24.1", 36 | "copy-webpack-plugin": "^5.1.1", 37 | "eslint": "^5.10.0", 38 | "eslint-config-airbnb": "^17.1.0", 39 | "eslint-plugin-import": "^2.14.0", 40 | "eslint-plugin-jsx-a11y": "^6.1.2", 41 | "eslint-plugin-react": "^7.17.0", 42 | "html-webpack-plugin": "^3.2.0", 43 | "progress-bar-webpack-plugin": "^1.11.0", 44 | "terser-webpack-plugin": "^2.3.2", 45 | "webpack": "^4.27.1", 46 | "webpack-cli": "^3.1.2", 47 | "webpack-dev-server": "^3.1.10" 48 | }, 49 | "dependencies": { 50 | "axios": "*", 51 | "jsonwebtoken": "*", 52 | "keycloak-js": "*" 53 | }, 54 | "eslintConfig": { 55 | "parser": "babel-eslint", 56 | "parserOptions": { 57 | "ecmaVersion": 7, 58 | "sourceType": "module", 59 | "ecmaFeatures": { 60 | "jsx": true 61 | } 62 | }, 63 | "plugins": [], 64 | "extends": "airbnb", 65 | "rules": { 66 | "no-undef": 0, 67 | "import/extensions": 0, 68 | "import/prefer-default-export": 0, 69 | "import/no-extraneous-dependencies": 0, 70 | "react/jsx-indent": 0 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /source/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 6 | const path = require('path'); 7 | 8 | const progressBarPlugin = new ProgressBarPlugin(); 9 | 10 | const env = process.env.NODE_ENV || 'development'; 11 | 12 | 13 | const copyConfigs = []; 14 | 15 | if (env === 'production') { 16 | copyConfigs.push({ 17 | from: './prod/login.html', 18 | to: 'login.html', 19 | }); 20 | } else { 21 | copyConfigs.push({ 22 | from: './dev/keycloak.json', 23 | to: 'keycloak.json', 24 | }); 25 | copyConfigs.push({ 26 | from: './dev/indexDev.js', 27 | to: 'index.js', 28 | }); 29 | copyConfigs.push({ 30 | from: './md5.js', 31 | to: 'md5.js', 32 | }); 33 | } 34 | 35 | const plugins = [ 36 | new webpack.optimize.OccurrenceOrderPlugin(true), 37 | new CopyWebpackPlugin(copyConfigs), 38 | // copyConfig, 39 | progressBarPlugin, 40 | ]; 41 | 42 | if (env !== 'production') { 43 | const htmlPlugin = new HtmlWebPackPlugin({ 44 | template: './dev/login.html', 45 | filename: './login.html', 46 | }); 47 | plugins.push(htmlPlugin); 48 | } 49 | 50 | const optimization = {}; 51 | if (env === 'production') { 52 | optimization.minimize = true; 53 | optimization.namedModules = false; 54 | optimization.namedChunks = false; 55 | optimization.mangleWasmImports = true; 56 | optimization.moduleIds = 'hashed'; 57 | optimization.minimizer = [new TerserPlugin()]; 58 | } 59 | 60 | module.exports = { 61 | output: { 62 | path: path.resolve(__dirname, '..', 'mikrotik'), 63 | filename: 'authorization.js', 64 | }, 65 | mode: env, 66 | devServer: { 67 | contentBase: 'public', 68 | historyApiFallback: true, 69 | hot: false, 70 | inline: false, 71 | host: '0.0.0.0', 72 | disableHostCheck: true, 73 | }, 74 | module: { 75 | rules: [ 76 | { 77 | test: /\.js$/, 78 | exclude: /node_modules/, 79 | use: { 80 | loader: 'babel-loader', 81 | }, 82 | }, 83 | { 84 | test: /\.css$/, 85 | use: [ 86 | { 87 | loader: 'style-loader', 88 | }, 89 | { 90 | loader: 'css-loader', 91 | options: { 92 | modules: true, 93 | importLoaders: 1, 94 | }, 95 | }, 96 | ], 97 | }, 98 | ], 99 | }, 100 | plugins, 101 | optimization, 102 | }; 103 | -------------------------------------------------------------------------------- /source/dev/login.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | internet hotspot > login 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 63 | 64 |
44 |
Please log on to use the internet hotspot service

45 | 46 | 47 | 56 | 57 | 58 |
48 |
49 | 50 | 51 | 52 | 53 |
 
54 |
55 |
mikrotik
59 | 60 |
Powered by MikroTik RouterOS
61 |
62 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /source/src/base/keyCloakCerts.js: -------------------------------------------------------------------------------- 1 | import { fetchData } from './restCalls'; 2 | 3 | const BEGIN_KEY = '-----BEGIN RSA PUBLIC KEY-----\n'; 4 | const END_KEY = '\n-----END RSA PUBLIC KEY-----\n'; 5 | 6 | function parse(response) { 7 | return new Promise((resolve, reject) => { 8 | try { 9 | const parsedData = JSON.parse(response); 10 | resolve(parsedData); 11 | } catch (e) { 12 | reject(e); 13 | } 14 | }); 15 | } 16 | 17 | function getJson(url) { 18 | return new Promise((resolve, reject) => { 19 | fetchData(url, 'GET').then((res) => { 20 | parse(res.data) 21 | .then(result => resolve(result)) 22 | .catch(error => reject(error)); 23 | }).catch((e) => { 24 | reject(new Error(`Status: ${e.statusCode}`)); 25 | }); 26 | }); 27 | } 28 | 29 | function getKey(response, kid) { 30 | return Object.hasOwnProperty.call(response, 'keys') 31 | ? response.keys.find(k => k.kid === kid) 32 | : undefined; 33 | } 34 | 35 | async function getKeyFromKeycloak(url, kid) { 36 | const response = await getJson(url); 37 | const key = getKey(response, kid); 38 | if (!key) { 39 | throw new Error(`Can't find key for kid "${kid}" in response.`); 40 | } 41 | return key; 42 | } 43 | 44 | function verify(key) { 45 | if (!(key.n && key.e)) { 46 | throw new Error('Can\'t find modulus or exponent in key.'); 47 | } 48 | if (key.kty !== 'RSA') { 49 | throw new Error('Key type (kty) must be RSA.'); 50 | } 51 | if (key.alg !== 'RS256') { 52 | throw new Error('Algorithm (alg) must be RS256.'); 53 | } 54 | } 55 | 56 | // Based on tracker1's node-rsa-pem-from-mod-exp module. 57 | // See https://github.com/tracker1/node-rsa-pem-from-mod-exp 58 | 59 | function convertToHex(str) { 60 | const hex = Buffer.from(str, 'base64').toString('hex'); 61 | return hex[0] < '0' || hex[0] > '7' 62 | ? `00${hex}` 63 | : hex; 64 | } 65 | 66 | function toHex(number) { 67 | const str = number.toString(16); 68 | return (str.length % 2) 69 | ? `0${str}` 70 | : str; 71 | } 72 | 73 | function toLongHex(number) { 74 | const str = toHex(number); 75 | const lengthByteLength = 128 + (str.length / 2); 76 | return toHex(lengthByteLength) + str; 77 | } 78 | 79 | function encodeLenght(n) { 80 | return n <= 127 81 | ? toHex(n) 82 | : toLongHex(n); 83 | } 84 | 85 | function getPublicKey(modulus, exponent) { 86 | const mod = convertToHex(modulus); 87 | const exp = convertToHex(exponent); 88 | const encModLen = encodeLenght(mod.length / 2); 89 | const encExpLen = encodeLenght(exp.length / 2); 90 | const part = [mod, exp, encModLen, encExpLen].map(n => n.length / 2).reduce((a, b) => a + b); 91 | const bufferSource = `30${encodeLenght(part + 2)}02${encModLen}${mod}02${encExpLen}${exp}`; 92 | const pubkey = Buffer.from(bufferSource, 'hex').toString('base64'); 93 | return BEGIN_KEY + pubkey.match(/.{1,64}/g).join('\n') + END_KEY; 94 | } 95 | async function fetch(url, kid) { 96 | const key = await getKeyFromKeycloak(url, kid); 97 | 98 | verify(key); 99 | return getPublicKey(key.n, key.e); 100 | } 101 | 102 | // eslint-disable-next-line import/prefer-default-export 103 | export async function KeycloakPublicKeyFetcher(url, realm, kid) { 104 | const certsUrl = realm ? `${url}/realms/${realm}/protocol/openid-connect/certs` : url; 105 | // eslint-disable-next-line no-return-await 106 | return await fetch(certsUrl, kid); 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/vzakharchenko/mikrotik-hotspot-oauth.svg?style=svg)](https://circleci.com/gh/vzakharchenko/mikrotik-hotspot-oauth) 2 | 3 | # Keycloak Radius Hotspot 4 | Setup social and other Oauth/Saml integration with [Keycloak Radius embedded server](https://github.com/vzakharchenko/keycloak-radius-plugin/releases) 5 | # How Keycloak Radius Hotspot works 6 | 1. Authorization through Keycloak occurs by [OpenID Connect](https://www.keycloak.org/docs/latest/securing_apps/#openid-connect-2). 7 | 2. User selects on the login page the identity provider through which he wants to log in 8 | 3. The result of a successful authorization is a JWT that contains a temporary session key. 9 | 4. With this key, the User is authorized through Radius Server. 10 | 5. Radius Server checks if this key is in the user session. And whether it was used. 11 | 6. Radius Server successfully authorizing the user 12 | 13 | # connection Schema`s 14 | 15 | ## - Cloud connection (Better to Use Radsec) 16 | ![KeycloakRadius (1)](/docs/KeycloakRadius.png) 17 | 18 | 19 | ## - Proxy connection 20 | ![KeycloakRadius](/docs/KeycloakRadius2.png) 21 | 22 | # Setup, build and configure HotSpot page for Social Login 23 | 24 | 1. Create Realm ![hotspotRealm](/docs/hotspotRealm.png) 25 | 2. create Radius Client ![RadiusClientHotSpot](/docs/RadiusClientHotSpot.png) 26 | 3. create OpenId client ![hotspotClient](/docs/hotspotClient.png) 27 | 4. Setting your Hotspot DNS in "Valid Redirect URIs" and "Web Origins" ![HotspotClientConfiguration](/docs/HotspotClientConfiguration.png) 28 | 5. add "Radius Session Password" Mapper ![HotSpotMapper](/docs/HotSpotMapper.png) ![HotSpotMapper2](/docs/HotSpotMapper2_1.png) 29 | 6. Download keycloak.json ![downloadKeycloakJson](/docs/downloadKeycloakJson.png) 30 | 31 | ## Setup Mikrotik 32 | 1. Upload all files from [hotspot/mikrotik](mikrotik) to flash/hotspot on device ([authorization.js](mikrotik/authorization.js) and [login.html](mikrotik/login.html)) 33 | - Using web UI 34 | - Using scp 35 | - Using ftp 36 | - Using winbox 37 | 2. Download keycloak.json ![downloadKeycloakJson](/docs/downloadKeycloakJson.png) 38 | 3. upload keycloak.json into flash/hotspot on device 39 | 4. update Walled Garden. Add your keycloak host ![addWalledGarden](/docs/addWalledGarden.png) ![KeycloakHostName](/docs/KeycloakHostName.png) 40 | 41 | ## Facebook Login example 42 | 1. [Install Keycloak with embedded Radius Server](https://github.com/vzakharchenko/keycloak-radius-plugin#release-setup) 43 | 2. install [ngrok](https://ngrok.com/). Register ngrok
./ngrok authtoken \
44 | 3. start ngrok
./ngrok http 8090
![Ngrok](/docs/Ngrok.png) 45 | 4. open keycloak goto realm and add Facebook Identity Provider ![SelectFacbook](/docs/SelectFacbook.png) 46 | 5. Copy Redirect URI ![Copy Redirect URI](/docs/Copy%20Redirect%20URI.png) 47 | 6. goto [https://developers.facebook.com/](https://developers.facebook.com/) and create a new application ![CreateApp1](/docs/CreateApp1.png)![CreateApp2](/docs/CreateApp2.png)![FacebookLogin3](/docs/FacebookLogin3.png)![Facebook4](/docs/Facebook4.png) 48 | 7. Insert Redirect URI from [Step 7](#L43) ![Facebook5](/docs/Facebook5.png) 49 | 8. Get App Id and Secret from application (Settings->basic) ![Facebook6](/docs/Facebook6.png) 50 | 9. back to Keycloak and set this App Id and Secret ![Facebook7](/docs/Facebook7.png) 51 | 10. add facebook hosts to Walled Garden ![FacebookWalledGarden](/docs/FacebookWalledGarden.png) 52 | ``` 53 | /ip hotspot walled-garden 54 | add comment=facebook dst-host=facebook.* 55 | add comment=facebook dst-host=*.facebook.* 56 | add comment=facebook dst-host=*.fbcdn.* 57 | add comment=facebook dst-host=*akamai* 58 | add comment=facebook dst-host=*atdmt* 59 | add comment=facebook dst-host=*fbsbx* 60 | add comment=common dst-host=www.google-analytics.com 61 | ``` 62 | 63 | 12. open hotspot page ![FacebookLoginHotspot](/docs/FacebookLoginHotspot.png) ![FacebookLogin2](/docs/FacebookLogin2.png) 64 | 65 | 66 | 67 | ## build UI 68 | 69 | ### build UI Requirements: 70 | node and npm must be installed 71 | macbook instalation [brew](https://brew.sh/) : *brew install node* 72 | [Install node on ubuntu ](https://linuxize.com/post/how-to-install-node-js-on-ubuntu-18.04/) 73 | 74 | ### Building steps 75 | 1. cd [./source](source) 76 | 2. npm i 77 | 3. npm run build 78 | result in [./mikrotik](mikrotik) 79 | -------------------------------------------------------------------------------- /mikrotik/login.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | internet hotspot > login 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 78 | 79 | 80 | 81 | 101 | 102 |
82 |
Please log on to use the internet hotspot service
$(if trial == 'yes')Free trial available, click here.$(endif)

83 | 84 | 85 | 94 | 95 | 96 |
86 |
87 | 88 | 89 | 90 | 91 |
 
92 |
93 |
mikrotik
97 | 98 |
Powered by MikroTik RouterOS
99 |
100 |
103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /source/prod/login.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | internet hotspot > login 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 78 | 79 | 80 | 81 | 101 | 102 |
82 |
Please log on to use the internet hotspot service
$(if trial == 'yes')Free trial available, click here.$(endif)

83 | 84 | 85 | 94 | 95 | 96 |
86 |
87 | 88 | 89 | 90 | 91 |
 
92 |
93 |
mikrotik
97 | 98 |
Powered by MikroTik RouterOS
99 |
100 |
103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /source/README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/vzakharchenko/mikrotik-hotspot-oauth.svg?style=svg)](https://circleci.com/gh/vzakharchenko/mikrotik-hotspot-oauth) 2 | [![donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://secure.wayforpay.com/button/be27056b0a2b4) 3 | 4 | # Keycloak Radius Hotspot 5 | Setup social and other Oauth/Saml integration with [Keycloak Radius embedded server](https://github.com/vzakharchenko/keycloak-radius-plugin/releases) 6 | # How Keycloak Radius Hotspot works 7 | 1. Authorization through Keycloak occurs by [OpenID Connect](https://www.keycloak.org/docs/latest/securing_apps/#openid-connect-2). 8 | 2. User selects on the login page the identity provider through which he wants to log in 9 | 3. The result of a successful authorization is a JWT that contains a temporary session key. 10 | 4. With this key, the User is authorized through Radius Server. 11 | 5. Radius Server checks if this key is in the user session. And whether it was used. 12 | 6. Radius Server successfully authorizing the user 13 | 14 | # connection Schema`s 15 | 16 | ## - Cloud connection (Better to Use Radsec) 17 | ![KeycloakRadius (1)](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/KeycloakRadius.png) 18 | 19 | 20 | ## - Proxy connection 21 | ![KeycloakRadius](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/KeycloakRadius2.png) 22 | 23 | # Setup, build and configure HotSpot page for Social Login 24 | 25 | 1. Create Realm ![hotspotRealm](/docs/hotspotRealm.png) 26 | 2. create Radius Client ![RadiusClientHotSpot](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/RadiusClientHotSpot.png) 27 | 3. create OpenId client ![hotspotClient](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/hotspotClient.png) 28 | 4. Setting your Hotspot DNS in "Valid Redirect URIs" and "Web Origins" ![HotspotClientConfiguration](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/HotspotClientConfiguration.png) 29 | 5. add "Radius Session Password" Mapper ![HotSpotMapper](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/HotSpotMapper.png) ![HotSpotMapper2](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/HotSpotMapper2_1.png) 30 | 6. Download keycloak.json ![downloadKeycloakJson](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/downloadKeycloakJson.png) 31 | 32 | ## Setup Mikrotik 33 | 1. Upload all files from [hotspot/mikrotik](mikrotik) to flash/hotspot on device ([authorization.js](mikrotik/authorization.js) and [login.html](mikrotik/login.html)) 34 | - Using web UI 35 | - Using scp 36 | - Using ftp 37 | - Using winbox 38 | 2. Download keycloak.json ![downloadKeycloakJson](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/downloadKeycloakJson.png) 39 | 3. upload keycloak.json into flash/hotspot on device 40 | 4. update Walled Garden. Add your keycloak host ![addWalledGarden](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/addWalledGarden.png) ![KeycloakHostName](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/KeycloakHostName.png) 41 | 42 | ## Facebook Login example 43 | 1. [Install Keycloak with embedded Radius Server](https://github.com/vzakharchenko/keycloak-radius-plugin#release-setup) 44 | 2. install [ngrok](https://ngrok.com/). Register ngrok
./ngrok authtoken \
45 | 3. start ngrok
./ngrok http 8090
![Ngrok](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Ngrok.png) 46 | 4. open keycloak goto realm and add Facebook Identity Provider ![SelectFacbook](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/SelectFacbook.png) 47 | 5. Copy Redirect URI ![Copy Redirect URI](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Copy%20Redirect%20URI.png) 48 | 6. goto [https://developers.facebook.com/](https://developers.facebook.com/) and create a new application ![CreateApp1](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/CreateApp1.png)![CreateApp2](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/CreateApp2.png)![FacebookLogin3](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/FacebookLogin3.png)![Facebook4](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Facebook4.png) 49 | 7. Insert Redirect URI from [Step 7](#L43) ![Facebook5](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Facebook5.png) 50 | 8. Get App Id and Secret from application (Settings->basic) ![Facebook6](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Facebook6.png) 51 | 9. back to Keycloak and set this App Id and Secret ![Facebook7](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/Facebook7.png) 52 | 10. add facebook hosts to Walled Garden ![FacebookWalledGarden](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/FacebookWalledGarden.png) 53 | ``` 54 | /ip hotspot walled-garden 55 | add comment=facebook dst-host=facebook.* 56 | add comment=facebook dst-host=*.facebook.* 57 | add comment=facebook dst-host=*.fbcdn.* 58 | add comment=facebook dst-host=*akamai* 59 | add comment=facebook dst-host=*atdmt* 60 | add comment=facebook dst-host=*fbsbx* 61 | add comment=common dst-host=www.google-analytics.com 62 | ``` 63 | 64 | 12. open hotspot page ![FacebookLoginHotspot](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/FacebookLoginHotspot.png) ![FacebookLogin2](https://github.com/vzakharchenko/mikrotik-hotspot-oauth/raw/master/docs/FacebookLogin2.png) 65 | 66 | 67 | 68 | ## build UI 69 | 70 | ### build UI Requirements: 71 | node and npm must be installed 72 | macbook instalation [brew](https://brew.sh/) : *brew install node* 73 | [Install node on ubuntu ](https://linuxize.com/post/how-to-install-node-js-on-ubuntu-18.04/) 74 | 75 | ### Building steps 76 | 1. cd [./source](source) 77 | 2. npm i 78 | 3. npm run build 79 | result in [./mikrotik](mikrotik) 80 | 81 | # If you find these useful, please [Donate](https://secure.wayforpay.com/button/be27056b0a2b4)! 82 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------