19 changed files with 163 additions and 1800 deletions
@ -1,37 +0,0 @@
@@ -1,37 +0,0 @@
|
||||
import { Request, Response } from "express"; |
||||
import Stacker from "../middlewares/stacker"; |
||||
import { GetUserMiddleware } from "../middlewares/user"; |
||||
import { URL } from "url"; |
||||
import Client from "../../models/client"; |
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error"; |
||||
import { getAccessTokenJWT } from "../../helper/jwt"; |
||||
|
||||
export const GetJWTByUser = Stacker( |
||||
GetUserMiddleware(true, false), |
||||
async (req: Request, res: Response) => { |
||||
const { client_id, origin } = req.query as { [key: string]: string }; |
||||
|
||||
const client = await Client.findOne({ |
||||
client_id, |
||||
}); |
||||
|
||||
const clientNotFoundError = new RequestError( |
||||
"Client not found!", |
||||
HttpStatusCode.BAD_REQUEST |
||||
); |
||||
|
||||
if (!client) throw clientNotFoundError; |
||||
|
||||
const clientUrl = new URL(client.redirect_url); |
||||
|
||||
if (clientUrl.hostname !== origin) throw clientNotFoundError; |
||||
|
||||
const jwt = await getAccessTokenJWT({ |
||||
user: req.user, |
||||
client: client, |
||||
permissions: [], |
||||
}); |
||||
|
||||
res.json({ jwt }); |
||||
} |
||||
); |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; |
||||
import Client, { IClient } from "../../../models/client"; |
||||
|
||||
export async function getClientWithOrigin(client_id: string, origin: string) { |
||||
const client = await Client.findOne({ |
||||
client_id, |
||||
}); |
||||
|
||||
const clientNotFoundError = new RequestError( |
||||
"Client not found!", |
||||
HttpStatusCode.BAD_REQUEST |
||||
); |
||||
|
||||
if (!client) throw clientNotFoundError; |
||||
|
||||
const clientUrl = new URL(client.redirect_url); |
||||
|
||||
if (clientUrl.hostname !== origin) throw clientNotFoundError; |
||||
|
||||
return client; |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
import { Router } from "express"; |
||||
import { GetJWTByUser } from "./jwt"; |
||||
import { GetPermissionsForAuthRequest } from "./permissions"; |
||||
import { GetTokenByUser } from "./refresh_token"; |
||||
|
||||
const router = Router(); |
||||
|
||||
router.get("/jwt", GetJWTByUser); |
||||
router.get("/permissions", GetPermissionsForAuthRequest); |
||||
router.get("/refresh_token", GetTokenByUser); |
||||
|
||||
export default router; |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
import { Request, Response } from "express"; |
||||
import Stacker from "../../middlewares/stacker"; |
||||
import { GetUserMiddleware } from "../../middlewares/user"; |
||||
import { URL } from "url"; |
||||
import Client from "../../../models/client"; |
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; |
||||
import { getAccessTokenJWT } from "../../../helper/jwt"; |
||||
import { getClientWithOrigin } from "./_helper"; |
||||
|
||||
export const GetJWTByUser = Stacker( |
||||
GetUserMiddleware(true, false), |
||||
async (req: Request, res: Response) => { |
||||
const { client_id, origin } = req.query as { [key: string]: string }; |
||||
|
||||
const client = await getClientWithOrigin(client_id, origin); |
||||
|
||||
const jwt = await getAccessTokenJWT({ |
||||
user: req.user, |
||||
client: client, |
||||
permissions: [], |
||||
}); |
||||
|
||||
res.json({ jwt }); |
||||
} |
||||
); |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
import { Request, Response } from "express"; |
||||
import Stacker from "../../middlewares/stacker"; |
||||
import { GetUserMiddleware } from "../../middlewares/user"; |
||||
import { URL } from "url"; |
||||
import Client from "../../../models/client"; |
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; |
||||
import { randomBytes } from "crypto"; |
||||
import moment = require("moment"); |
||||
import RefreshToken from "../../../models/refresh_token"; |
||||
import { refreshTokenValidTime } from "../../../config"; |
||||
import { getClientWithOrigin } from "./_helper"; |
||||
import Permission from "../../../models/permissions"; |
||||
|
||||
export const GetPermissionsForAuthRequest = Stacker( |
||||
GetUserMiddleware(true, false), |
||||
async (req: Request, res: Response) => { |
||||
const { client_id, origin, permissions } = req.query as { |
||||
[key: string]: string; |
||||
}; |
||||
|
||||
const client = await getClientWithOrigin(client_id, origin); |
||||
|
||||
const perm = permissions.split(",").filter((e) => !!e); |
||||
|
||||
const resolved = await Promise.all( |
||||
perm.map((p) => Permission.findById(p)) |
||||
); |
||||
|
||||
if (resolved.some((e) => e.grant_type !== "user")) { |
||||
throw new RequestError( |
||||
"Invalid Permission requested", |
||||
HttpStatusCode.BAD_REQUEST |
||||
); |
||||
} |
||||
|
||||
res.json({ permissions: resolved }); |
||||
} |
||||
); |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
import { Request, Response } from "express"; |
||||
import Stacker from "../../middlewares/stacker"; |
||||
import { GetUserMiddleware } from "../../middlewares/user"; |
||||
import { URL } from "url"; |
||||
import Client from "../../../models/client"; |
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error"; |
||||
import { randomBytes } from "crypto"; |
||||
import moment = require("moment"); |
||||
import RefreshToken from "../../../models/refresh_token"; |
||||
import { refreshTokenValidTime } from "../../../config"; |
||||
import { getClientWithOrigin } from "./_helper"; |
||||
import Permission from "../../../models/permissions"; |
||||
|
||||
export const GetTokenByUser = Stacker( |
||||
GetUserMiddleware(true, false), |
||||
async (req: Request, res: Response) => { |
||||
const { client_id, origin, permissions } = req.query as { |
||||
[key: string]: string; |
||||
}; |
||||
|
||||
const client = await getClientWithOrigin(client_id, origin); |
||||
|
||||
const perm = permissions.split(",").filter((e) => !!e); |
||||
|
||||
const resolved = await Promise.all( |
||||
perm.map((p) => Permission.findById(p)) |
||||
); |
||||
|
||||
if (resolved.some((e) => e.grant_type !== "user")) { |
||||
throw new RequestError( |
||||
"Invalid Permission requested", |
||||
HttpStatusCode.BAD_REQUEST |
||||
); |
||||
} |
||||
|
||||
let token = RefreshToken.new({ |
||||
user: req.user._id, |
||||
client: client._id, |
||||
permissions: resolved.map((e) => e._id), |
||||
token: randomBytes(16).toString("hex"), |
||||
valid: true, |
||||
validTill: moment().add(refreshTokenValidTime).toDate(), |
||||
}); |
||||
|
||||
await RefreshToken.save(token); |
||||
|
||||
res.json({ token }); |
||||
} |
||||
); |
@ -1,56 +0,0 @@
@@ -1,56 +0,0 @@
|
||||
<html> |
||||
|
||||
<head> |
||||
<title>{{i18n "Login"}}</title> |
||||
<meta charset="utf8" /> |
||||
<meta name="viewport" content="width=device-width,initial-scale=1" /> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="content"></div> |
||||
{{!-- <hgroup> |
||||
<h1>{{i18n "Login"}}</h1> |
||||
</hgroup> |
||||
<form action="JavaScript:void(0)"> |
||||
<div class="loader_box" id="loader"> |
||||
<div class="loader"></div> |
||||
</div> |
||||
<div id="container"> |
||||
<div id="usernamegroup"> |
||||
<div class="floating group"> |
||||
<input type="text" id="username" autofocus> |
||||
<span class="highlight"></span> |
||||
<span class="bar"></span> |
||||
<label>{{i18n "Username or Email"}}</label> |
||||
<div class="error invisible" id="uerrorfield"></div> |
||||
</div> |
||||
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}} |
||||
</button> |
||||
</div> |
||||
<div id="passwordgroup"> |
||||
<div class="floating group invisible" id="passwordgroup"> |
||||
<input type="password" id="password"> |
||||
<span class="highlight"></span> |
||||
<span class="bar"></span> |
||||
<label>{{i18n "Password"}}</label> |
||||
<div class="error invisible" id="perrorfield"></div> |
||||
</div> |
||||
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised">{{i18n "Login"}} |
||||
</button> |
||||
</div> |
||||
<div id="twofactorgroup"> |
||||
<ul id="tflist"> |
||||
</ul> |
||||
|
||||
<div id="tfinput"> |
||||
|
||||
</div> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
<footer> |
||||
<p>Powered by {{appname}}</p> |
||||
</footer> --}} |
||||
</body> |
||||
|
||||
</html> |
@ -1,166 +0,0 @@
@@ -1,166 +0,0 @@
|
||||
import sha from "sha512"; |
||||
import { setCookie, getCookie } from "cookie"; |
||||
import "inputs"; |
||||
|
||||
const loader = document.getElementById("loader"); |
||||
const container = document.getElementById("container"); |
||||
const usernameinput = document.getElementById("username"); |
||||
const usernamegroup = document.getElementById("usernamegroup"); |
||||
const uerrorfield = document.getElementById("uerrorfield"); |
||||
const passwordinput = document.getElementById("password"); |
||||
const passwordgroup = document.getElementById("passwordgroup"); |
||||
const perrorfield = document.getElementById("perrorfield"); |
||||
const nextbutton = document.getElementById("nextbutton"); |
||||
const loginbutton = document.getElementById("loginbutton"); |
||||
|
||||
let username; |
||||
let salt; |
||||
|
||||
usernameinput.focus(); |
||||
|
||||
const loading = () => { |
||||
container.style.filter = "blur(2px)"; |
||||
loader.style.display = ""; |
||||
}; |
||||
|
||||
const loading_fin = () => { |
||||
container.style.filter = ""; |
||||
loader.style.display = "none"; |
||||
}; |
||||
loading_fin(); |
||||
|
||||
usernameinput.onkeydown = (e) => { |
||||
var keycode = e.keyCode ? e.keyCode : e.which; |
||||
if (keycode === 13) nextbutton.click(); |
||||
clearError(uerrorfield); |
||||
}; |
||||
|
||||
nextbutton.onclick = async () => { |
||||
loading(); |
||||
username = usernameinput.value; |
||||
try { |
||||
let res = await fetch( |
||||
"/api/user/login?type=username&username=" + username, |
||||
{ |
||||
method: "POST", |
||||
} |
||||
) |
||||
.then((e) => { |
||||
if (e.status !== 200) throw new Error(e.statusText); |
||||
return e.json(); |
||||
}) |
||||
.then((data) => { |
||||
if (data.error) { |
||||
return Promise.reject(new Error(data.error)); |
||||
} |
||||
return data; |
||||
}); |
||||
salt = res.salt; |
||||
usernamegroup.classList.add("invisible"); |
||||
passwordgroup.classList.remove("invisible"); |
||||
passwordinput.focus(); |
||||
} catch (e) { |
||||
showError(uerrorfield, e.message); |
||||
} |
||||
loading_fin(); |
||||
}; |
||||
|
||||
passwordinput.onkeydown = (e) => { |
||||
var keycode = e.keyCode ? e.keyCode : e.which; |
||||
if (keycode === 13) loginbutton.click(); |
||||
clearError(perrorfield); |
||||
}; |
||||
|
||||
loginbutton.onclick = async () => { |
||||
loading(); |
||||
let pw = sha(salt + passwordinput.value); |
||||
try { |
||||
let { login, special, tfa } = await fetch( |
||||
"/api/user/login?type=password", |
||||
{ |
||||
method: "POST", |
||||
body: JSON.stringify({ |
||||
username: usernameinput.value, |
||||
password: pw, |
||||
}), |
||||
headers: { |
||||
"content-type": "application/json", |
||||
}, |
||||
} |
||||
) |
||||
.then((e) => { |
||||
if (e.status !== 200) throw new Error(e.statusText); |
||||
return e.json(); |
||||
}) |
||||
.then((data) => { |
||||
if (data.error) { |
||||
return Promise.reject(new Error(data.error)); |
||||
} |
||||
return data; |
||||
}); |
||||
|
||||
setCookie("login", login.token, new Date(login.expires).toUTCString()); |
||||
setCookie( |
||||
"special", |
||||
special.token, |
||||
new Date(special.expires).toUTCString() |
||||
); |
||||
let d = new Date(); |
||||
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
|
||||
setCookie("username", username, d.toUTCString()); |
||||
let url = new URL(window.location.href); |
||||
let state = url.searchParams.get("state"); |
||||
let red = "/"; |
||||
|
||||
if (tfa) twofactor(tfa); |
||||
else { |
||||
if (state) { |
||||
let base64 = url.searchParams.get("base64"); |
||||
if (base64) red = atob(state); |
||||
else red = state; |
||||
} |
||||
window.location.href = red; |
||||
} |
||||
} catch (e) { |
||||
passwordinput.value = ""; |
||||
showError(perrorfield, e.message); |
||||
} |
||||
loading_fin(); |
||||
}; |
||||
|
||||
function clearError(field) { |
||||
field.innerText = ""; |
||||
field.classList.add("invisible"); |
||||
} |
||||
|
||||
function showError(field, error) { |
||||
field.innerText = error; |
||||
field.classList.remove("invisible"); |
||||
} |
||||
|
||||
username = getCookie("username"); |
||||
if (username) { |
||||
usernameinput.value = username; |
||||
|
||||
var evt = document.createEvent("HTMLEvents"); |
||||
evt.initEvent("change", false, true); |
||||
usernameinput.dispatchEvent(evt); |
||||
} |
||||
|
||||
function twofactor(tfa) { |
||||
let list = tfa |
||||
.map((entry) => { |
||||
switch (entry) { |
||||
case 0: // OTC
|
||||
return "Authenticator App"; |
||||
case 1: // BACKUP
|
||||
return "Backup Key"; |
||||
} |
||||
return undefined; |
||||
}) |
||||
.filter((e) => e !== undefined) |
||||
.reduce((p, c) => p + `<li>${c}</li>`, ""); |
||||
|
||||
let tfl = document.getElementById("tflist"); |
||||
tfl.innerHTML = list; |
||||
} |
@ -1,132 +0,0 @@
@@ -1,132 +0,0 @@
|
||||
@import "@material/button/mdc-button"; |
||||
@import "inputs"; |
||||
@import "style"; |
||||
|
||||
.spanned-btn { |
||||
width: 100%; |
||||
background: $primary !important; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5); |
||||
} |
||||
|
||||
* { |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
body { |
||||
font-family: Helvetica; |
||||
background: #eee; |
||||
-webkit-font-smoothing: antialiased; |
||||
} |
||||
|
||||
header { |
||||
text-align: center; |
||||
margin-top: 4em; |
||||
} |
||||
|
||||
h1, |
||||
h3 { |
||||
font-weight: 300; |
||||
} |
||||
|
||||
h1 { |
||||
color: #636363; |
||||
} |
||||
|
||||
h3 { |
||||
color: $primary; |
||||
} |
||||
|
||||
form { |
||||
max-width: 380px; |
||||
margin: 4em auto; |
||||
padding: 3em 2em 2em 2em; |
||||
background: #fafafa; |
||||
border: 1px solid #ebebeb; |
||||
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, |
||||
rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px; |
||||
position: relative; |
||||
} |
||||
|
||||
.loader_box { |
||||
width: 64px; |
||||
height: 64px; |
||||
margin: auto; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
bottom: 0; |
||||
right: 0; |
||||
} |
||||
|
||||
.loader { |
||||
display: inline-block; |
||||
position: relative; |
||||
z-index: 100; |
||||
} |
||||
.loader:after { |
||||
content: " "; |
||||
display: block; |
||||
width: 46px; |
||||
height: 46px; |
||||
margin: 1px; |
||||
border-radius: 50%; |
||||
border: 5px solid #000000; |
||||
border-color: #000000 transparent #000000 transparent; |
||||
animation: loader 1.2s linear infinite; |
||||
} |
||||
@keyframes loader { |
||||
0% { |
||||
transform: rotate(0deg); |
||||
} |
||||
100% { |
||||
transform: rotate(360deg); |
||||
} |
||||
} |
||||
|
||||
footer { |
||||
text-align: center; |
||||
} |
||||
|
||||
footer p { |
||||
color: #888; |
||||
font-size: 13px; |
||||
letter-spacing: 0.4px; |
||||
} |
||||
|
||||
footer a { |
||||
color: $primary; |
||||
text-decoration: none; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
footer a:hover { |
||||
color: #666; |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
footer img { |
||||
width: 80px; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
footer img:hover { |
||||
opacity: 0.83; |
||||
} |
||||
|
||||
footer img:focus, |
||||
footer a:focus { |
||||
outline: none; |
||||
} |
||||
|
||||
.invisible { |
||||
display: none; |
||||
} |
||||
|
||||
.errorColor { |
||||
background: $error !important; |
||||
} |
||||
|
||||
.error { |
||||
color: $error; |
||||
margin-top: 5px; |
||||
font-size: 13px; |
||||
} |
@ -1,434 +0,0 @@
@@ -1,434 +0,0 @@
|
||||
import { h, Component, render } from "preact"; |
||||
import "inputs"; |
||||
import "./u2f-api-polyfill"; |
||||
|
||||
import sha from "sha512"; |
||||
import { setCookie, getCookie } from "cookie"; |
||||
|
||||
let appname = "test"; |
||||
|
||||
function Loader() { |
||||
return ( |
||||
<div class="loader_box" id="loader"> |
||||
<div class="loader"></div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
class Username extends Component< |
||||
{ username: string; onNext: (username: string, salt: string) => void }, |
||||
{ error: string; loading: boolean } |
||||
> { |
||||
username_input: HTMLInputElement; |
||||
constructor() { |
||||
super(); |
||||
this.state = { error: undefined, loading: false }; |
||||
} |
||||
|
||||
async onClick() { |
||||
this.setState({ loading: true }); |
||||
try { |
||||
let res = await fetch( |
||||
"/api/user/login?type=username&username=" + |
||||
this.username_input.value, |
||||
{ |
||||
method: "POST", |
||||
} |
||||
) |
||||
.then((e) => { |
||||
if (e.status !== 200) throw new Error(e.statusText); |
||||
return e.json(); |
||||
}) |
||||
.then((data) => { |
||||
if (data.error) { |
||||
return Promise.reject(new Error(data.error)); |
||||
} |
||||
return data; |
||||
}); |
||||
let salt = res.salt; |
||||
this.props.onNext(this.username_input.value, salt); |
||||
} catch (err) { |
||||
this.setState({ |
||||
error: err.message, |
||||
}); |
||||
} |
||||
this.setState({ loading: false }); |
||||
} |
||||
|
||||
render() { |
||||
if (this.state.loading) return <Loader />; |
||||
return ( |
||||
<div> |
||||
<div class="floating group"> |
||||
<input |
||||
onKeyDown={(e) => { |
||||
let k = e.keyCode | e.which; |
||||
if (k === 13) this.onClick(); |
||||
this.setState({ error: undefined }); |
||||
}} |
||||
type="text" |
||||
value={ |
||||
this.username_input |
||||
? this.username_input.value |
||||
: this.props.username |
||||
} |
||||
autofocus |
||||
ref={(elm) => (elm ? (this.username_input = elm) : undefined)} |
||||
/> |
||||
<span class="highlight"></span> |
||||
<span class="bar"></span> |
||||
<label>Username or Email</label> |
||||
{this.state.error ? ( |
||||
<div class="error"> {this.state.error}</div> |
||||
) : undefined} |
||||
</div> |
||||
<button |
||||
type="button" |
||||
class="mdc-button mdc-button--raised spanned-btn" |
||||
onClick={() => this.onClick()} |
||||
> |
||||
Next |
||||
</button> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
enum TFATypes { |
||||
OTC, |
||||
BACKUP_CODE, |
||||
YUBI_KEY, |
||||
APP_ALLOW, |
||||
} |
||||
|
||||
interface TwoFactors { |
||||
id: string; |
||||
name: string; |
||||
type: TFATypes; |
||||
} |
||||
|
||||
class Password extends Component< |
||||
{ |
||||
username: string; |
||||
salt: string; |
||||
onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void; |
||||
}, |
||||
{ error: string; loading: boolean } |
||||
> { |
||||
password_input: HTMLInputElement; |
||||
constructor() { |
||||
super(); |
||||
this.state = { error: undefined, loading: false }; |
||||
} |
||||
|
||||
async onClick() { |
||||
this.setState({ |
||||
loading: true, |
||||
}); |
||||
|
||||
try { |
||||
let pw = sha(this.props.salt + this.password_input.value); |
||||
let { login, special, tfa } = await fetch( |
||||
"/api/user/login?type=password", |
||||
{ |
||||
method: "POST", |
||||
body: JSON.stringify({ |
||||
username: this.props.username, |
||||
password: pw, |
||||
}), |
||||
headers: { |
||||
"content-type": "application/json", |
||||
}, |
||||
} |
||||
) |
||||
.then((e) => { |
||||
if (e.status !== 200) throw new Error(e.statusText); |
||||
return e.json(); |
||||
}) |
||||
.then((data) => { |
||||
if (data.error) { |
||||
return Promise.reject(new Error(data.error)); |
||||
} |
||||
return data; |
||||
}); |
||||
this.props.onNext(login, special, tfa); |
||||
} catch (err) { |
||||
this.setState({ error: err.messagae }); |
||||
} |
||||
this.setState({ loading: false }); |
||||
} |
||||
|
||||
render() { |
||||
if (this.state.loading) return <Loader />; |
||||
return ( |
||||
<div> |
||||
<div class="floating group"> |
||||
<input |
||||
onKeyDown={(e) => { |
||||
let k = e.keyCode | e.which; |
||||
if (k === 13) this.onClick(); |
||||
this.setState({ error: undefined }); |
||||
}} |
||||
type="password" |
||||
ref={(elm: HTMLInputElement) => { |
||||
if (elm) { |
||||
this.password_input = elm; |
||||
setTimeout(() => elm.focus(), 200); |
||||
// elm.focus();
|
||||
} |
||||
}} |
||||
/> |
||||
<span class="highlight"></span> |
||||
<span class="bar"></span> |
||||
<label>Password</label> |
||||
{this.state.error ? ( |
||||
<div class="error"> {this.state.error}</div> |
||||
) : undefined} |
||||
</div> |
||||
<button |
||||
type="button" |
||||
class="mdc-button mdc-button--raised spanned-btn" |
||||
onClick={() => this.onClick()} |
||||
> |
||||
Login |
||||
</button> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
class TwoFactor extends Component< |
||||
{ twofactors: TwoFactors[]; next: (id: string, type: TFATypes) => void }, |
||||
{} |
||||
> { |
||||
render() { |
||||
let tfs = this.props.twofactors.map((fac) => { |
||||
let name: string; |
||||
switch (fac.type) { |
||||
case TFATypes.OTC: |
||||
name = "Authenticator"; |
||||
break; |
||||
case TFATypes.BACKUP_CODE: |
||||
name = "Backup code"; |
||||
break; |
||||
case TFATypes.APP_ALLOW: |
||||
name = "Use App: %s"; |
||||
break; |
||||
case TFATypes.YUBI_KEY: |
||||
name = "Use Yubikey: %s"; |
||||
break; |
||||
} |
||||
|
||||
name = name.replace("%s", fac.name ? fac.name : ""); |
||||
|
||||
return ( |
||||
<li |
||||
onClick={() => { |
||||
console.log("Click on Solution"); |
||||
this.props.next(fac.id, fac.type); |
||||
}} |
||||
> |
||||
{name} |
||||
</li> |
||||
); |
||||
}); |
||||
return ( |
||||
<div> |
||||
<h1>Select one</h1> |
||||
<ul>{tfs}</ul> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
// class TFA_YubiKey extends Component<{ id: string, login: Token, special: Token, next: (login: Token, special: Token) => void }, {}> {
|
||||
// render() {
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
enum Page { |
||||
username, |
||||
password, |
||||
twofactor, |
||||
yubikey, |
||||
} |
||||
|
||||
interface Token { |
||||
token: string; |
||||
expires: string; |
||||
} |
||||
|
||||
async function apiRequest( |
||||
endpoint: string, |
||||
method: "GET" | "POST" | "DELETE" | "PUT" = "GET", |
||||
body: string = undefined |
||||
) { |
||||
return fetch(endpoint, { |
||||
method, |
||||
body, |
||||
credentials: "same-origin", |
||||
headers: { |
||||
"content-type": "application/json", |
||||
}, |
||||
}) |
||||
.then((e) => { |
||||
if (e.status !== 200) throw new Error(e.statusText); |
||||
return e.json(); |
||||
}) |
||||
.then((data) => { |
||||
if (data.error) { |
||||
return Promise.reject(new Error(data.error)); |
||||
} |
||||
return data; |
||||
}); |
||||
} |
||||
|
||||
class App extends Component< |
||||
{}, |
||||
{ |
||||
page: Page; |
||||
username: string; |
||||
salt: string; |
||||
twofactor: TwoFactors[]; |
||||
twofactor_id: string; |
||||
} |
||||
> { |
||||
login: Token; |
||||
special: Token; |
||||
constructor() { |
||||
super(); |
||||
this.state = { |
||||
page: Page.username, |
||||
username: getCookie("username"), |
||||
salt: undefined, |
||||
twofactor: [], |
||||
twofactor_id: null, |
||||
}; |
||||
} |
||||
|
||||
setCookies() { |
||||
setCookie( |
||||
"login", |
||||
this.login.token, |
||||
new Date(this.login.expires).toUTCString() |
||||
); |
||||
setCookie( |
||||
"special", |
||||
this.special.token, |
||||
new Date(this.special.expires).toUTCString() |
||||
); |
||||
} |
||||
|
||||
finish() { |
||||
this.setCookies(); |
||||
let d = new Date(); |
||||
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
|
||||
setCookie("username", this.state.username, d.toUTCString()); |
||||
let url = new URL(window.location.href); |
||||
let state = url.searchParams.get("state"); |
||||
let red = "/"; |
||||
|
||||
if (state) { |
||||
let base64 = url.searchParams.get("base64"); |
||||
if (base64) red = atob(state); |
||||
else red = state; |
||||
} |
||||
window.location.href = red; |
||||
} |
||||
|
||||
render() { |
||||
let cont; |
||||
switch (this.state.page) { |
||||
case Page.username: |
||||
cont = ( |
||||
<Username |
||||
username={this.state.username} |
||||
onNext={(username, salt) => { |
||||
this.setState({ username, salt, page: Page.password }); |
||||
localStorage.setItem("username", username); |
||||
}} |
||||
/> |
||||
); |
||||
break; |
||||
case Page.password: |
||||
cont = ( |
||||
<Password |
||||
username={this.state.username} |
||||
salt={this.state.salt} |
||||
onNext={(login, special, twofactor) => { |
||||
this.login = login; |
||||
this.special = special; |
||||
this.setCookies(); |
||||
|
||||
if (!twofactor) { |
||||
this.finish(); |
||||
} else { |
||||
this.setState({ twofactor, page: Page.twofactor }); |
||||
} |
||||
}} |
||||
/> |
||||
); |
||||
break; |
||||
case Page.twofactor: |
||||
cont = ( |
||||
<TwoFactor |
||||
twofactors={this.state.twofactor} |
||||
next={async (id, type) => { |
||||
if (type === TFATypes.YUBI_KEY) { |
||||
let { request } = await apiRequest( |
||||
"/api/user/twofactor/yubikey", |
||||
"GET" |
||||
); |
||||
console.log(request); |
||||
(window as any).u2f.sign( |
||||
request.appId, |
||||
[request.challenge], |
||||
[request], |
||||
async (response) => { |
||||
let res = await apiRequest( |
||||
"/api/user/twofactor/yubikey", |
||||
"PUT", |
||||
JSON.stringify({ response }) |
||||
); |
||||
if (res.success) { |
||||
this.login.expires = res.login_exp; |
||||
this.special.expires = res.special_exp; |
||||
this.finish(); |
||||
} |
||||
} |
||||
); |
||||
} |
||||
}} |
||||
/> |
||||
); |
||||
break; |
||||
// case Page.yubikey:
|
||||
// cont = <TFA_YubiKey id={this.state.twofactor_id} login={this.login} special={this.special} next={(login, special) => {
|
||||
// this.login = login;
|
||||
// this.special = special;
|
||||
// this.finish()
|
||||
// }} />
|
||||
// break;
|
||||
} |
||||
return ( |
||||
<div> |
||||
<header> |
||||
<h1>Login</h1> |
||||
</header> |
||||
<form action="JavaScript:void(0)">{cont}</form> |
||||
<footer> |
||||
<p>Powered by {appname}</p> |
||||
</footer> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
document.addEventListener( |
||||
"DOMContentLoaded", |
||||
function () { |
||||
render(<App />, document.body.querySelector("#content")); |
||||
}, |
||||
false |
||||
); |
@ -1,844 +0,0 @@
@@ -1,844 +0,0 @@
|
||||
//Copyright 2014-2015 Google Inc. All rights reserved.
|
||||
|
||||
//Use of this source code is governed by a BSD-style
|
||||
//license that can be found in the LICENSE file or at
|
||||
//https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
// NOTE FROM MAINTAINER: This file is copied from google/u2f-ref-code with as
|
||||
// few alterations as possible. Any changes that were necessary are annotated
|
||||
// with "NECESSARY CHANGE". These changes, as well as this note, should be
|
||||
// preserved when updating this file from the source.
|
||||
|
||||
/** |
||||
* @fileoverview The U2F api. |
||||
*/ |
||||
"use strict"; |
||||
|
||||
// NECESSARY CHANGE: wrap the whole file in a closure
|
||||
(function () { |
||||
// NECESSARY CHANGE: detect UA to avoid clobbering other browser's U2F API.
|
||||
var isChrome = |
||||
"chrome" in window && window.navigator.userAgent.indexOf("Edge") < 0; |
||||
if ("u2f" in window || !isChrome) { |
||||
return; |
||||
} |
||||
|
||||
/** |
||||
* Namespace for the U2F api. |
||||
* @type {Object} |
||||
*/ |
||||
// NECESSARY CHANGE: define the window.u2f API.
|
||||
var u2f = (window.u2f = {}); |
||||
|
||||
/** |
||||
* FIDO U2F Javascript API Version |
||||
* @number |
||||
*/ |
||||
var js_api_version; |
||||
|
||||
/** |
||||
* The U2F extension id |
||||
* @const {string} |
||||
*/ |
||||
// The Chrome packaged app extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the package Chrome app and does not require installing the U2F Chrome extension.
|
||||
u2f.EXTENSION_ID = "kmendfapggjehodndflmmgagdbamhnfd"; |
||||
// The U2F Chrome extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the U2F Chrome extension to authenticate.
|
||||
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
||||
|
||||
/** |
||||
* Message types for messsages to/from the extension |
||||
* @const |
||||
* @enum {string} |
||||
*/ |
||||
u2f.MessageTypes = { |
||||
U2F_REGISTER_REQUEST: "u2f_register_request", |
||||
U2F_REGISTER_RESPONSE: "u2f_register_response", |
||||
U2F_SIGN_REQUEST: "u2f_sign_request", |
||||
U2F_SIGN_RESPONSE: "u2f_sign_response", |
||||
U2F_GET_API_VERSION_REQUEST: "u2f_get_api_version_request", |
||||
U2F_GET_API_VERSION_RESPONSE: "u2f_get_api_version_response", |
||||
}; |
||||
|
||||
/** |
||||
* Response status codes |
||||
* @const |
||||
* @enum {number} |
||||
*/ |
||||
u2f.ErrorCodes = { |
||||
OK: 0, |
||||
OTHER_ERROR: 1, |
||||
BAD_REQUEST: 2, |
||||
CONFIGURATION_UNSUPPORTED: 3, |
||||
DEVICE_INELIGIBLE: 4, |
||||
TIMEOUT: 5, |
||||
}; |
||||
|
||||
/** |
||||
* A message for registration requests |
||||
* @typedef {{ |
||||
* type: u2f.MessageTypes, |
||||
* appId: ?string, |
||||
* timeoutSeconds: ?number, |
||||
* requestId: ?number |
||||
* }} |
||||
*/ |
||||
u2f.U2fRequest; |
||||
|
||||
/** |
||||
* A message for registration responses |
||||
* @typedef {{ |
||||
* type: u2f.MessageTypes, |
||||
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), |
||||
* requestId: ?number |
||||
* }} |
||||
*/ |
||||
u2f.U2fResponse; |
||||
|
||||
/** |
||||
* An error object for responses |
||||
* @typedef {{ |
||||
* errorCode: u2f.ErrorCodes, |
||||
* errorMessage: ?string |
||||
* }} |
||||
*/ |
||||
u2f.Error; |
||||
|
||||
/** |
||||
* Data object for a single sign request. |
||||
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}} |
||||
*/ |
||||
u2f.Transport; |
||||
|
||||
/** |
||||
* Data object for a single sign request. |
||||
* @typedef {Array<u2f.Transport>} |
||||
*/ |
||||
u2f.Transports; |
||||
|
||||
/** |
||||
* Data object for a single sign request. |
||||
* @typedef {{ |
||||
* version: string, |
||||
* challenge: string, |
||||
* keyHandle: string, |
||||
* appId: string |
||||
* }} |
||||
*/ |
||||
u2f.SignRequest; |
||||
|
||||
/** |
||||
* Data object for a sign response. |
||||
* @typedef {{ |
||||
* keyHandle: string, |
||||
* signatureData: string, |
||||
* clientData: string |
||||
* }} |
||||
*/ |
||||
u2f.SignResponse; |
||||
|
||||
/** |
||||
* Data object for a registration request. |
||||
* @typedef {{ |
||||
* version: string, |
||||
* challenge: string |
||||
* }} |
||||
*/ |
||||
u2f.RegisterRequest; |
||||
|
||||
/** |
||||
* Data object for a registration response. |
||||
* @typedef {{ |
||||
* version: string, |
||||
* keyHandle: string, |
||||
* transports: Transports, |
||||
* appId: string |
||||
* }} |
||||
*/ |
||||
u2f.RegisterResponse; |
||||
|
||||
/** |
||||
* Data object for a registered key. |
||||
* @typedef {{ |
||||
* version: string, |
||||
* keyHandle: string, |
||||
* transports: ?Transports, |
||||
* appId: ?string |
||||
* }} |
||||
*/ |
||||
u2f.RegisteredKey; |
||||
|
||||
/** |
||||
* Data object for a get API register response. |
||||
* @typedef {{ |
||||
* js_api_version: number |
||||
* }} |
||||
*/ |
||||
u2f.GetJsApiVersionResponse; |
||||
|
||||
//Low level MessagePort API support
|
||||
|
||||
/** |
||||
* Sets up a MessagePort to the U2F extension using the |
||||
* available mechanisms. |
||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback |
||||
*/ |
||||
u2f.getMessagePort = function (callback) { |
||||
if (typeof chrome != "undefined" && chrome.runtime) { |
||||
// The actual message here does not matter, but we need to get a reply
|
||||
// for the callback to run. Thus, send an empty signature request
|
||||
// in order to get a failure response.
|
||||
var msg = { |
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST, |
||||
signRequests: [], |
||||
}; |
||||
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () { |
||||
if (!chrome.runtime.lastError) { |
||||
// We are on a whitelisted origin and can talk directly
|
||||
// with the extension.
|
||||
u2f.getChromeRuntimePort_(callback); |
||||
} else { |
||||
// chrome.runtime was available, but we couldn't message
|
||||
// the extension directly, use iframe
|
||||
u2f.getIframePort_(callback); |
||||
} |
||||
}); |
||||
} else if (u2f.isAndroidChrome_()) { |
||||
u2f.getAuthenticatorPort_(callback); |
||||
} else if (u2f.isIosChrome_()) { |
||||
u2f.getIosPort_(callback); |
||||
} else { |
||||
// chrome.runtime was not available at all, which is normal
|
||||
// when this origin doesn't have access to any extensions.
|
||||
u2f.getIframePort_(callback); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Detect chrome running on android based on the browser's useragent. |
||||
* @private |
||||
*/ |
||||
u2f.isAndroidChrome_ = function () { |
||||
var userAgent = navigator.userAgent; |
||||
return ( |
||||
userAgent.indexOf("Chrome") != -1 && userAgent.indexOf("Android") != -1 |
||||
); |
||||
}; |
||||
|
||||
/** |
||||
* Detect chrome running on iOS based on the browser's platform. |
||||
* @private |
||||
*/ |
||||
u2f.isIosChrome_ = function () { |
||||
return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; |
||||
}; |
||||
|
||||
/** |
||||
* Connects directly to the extension via chrome.runtime.connect. |
||||
* @param {function(u2f.WrappedChromeRuntimePort_)} callback |
||||
* @private |
||||
*/ |
||||
u2f.getChromeRuntimePort_ = function (callback) { |
||||
var port = chrome.runtime.connect(u2f.EXTENSION_ID, { |
||||
includeTlsChannelId: true, |
||||
}); |
||||
setTimeout(function () { |
||||
callback(new u2f.WrappedChromeRuntimePort_(port)); |
||||
}, 0); |
||||
}; |
||||
|
||||
/** |
||||
* Return a 'port' abstraction to the Authenticator app. |
||||
* @param {function(u2f.WrappedAuthenticatorPort_)} callback |
||||
* @private |
||||
*/ |
||||
u2f.getAuthenticatorPort_ = function (callback) { |
||||
setTimeout(function () { |
||||
callback(new u2f.WrappedAuthenticatorPort_()); |
||||
}, 0); |
||||
}; |
||||
|
||||
/** |
||||
* Return a 'port' abstraction to the iOS client app. |
||||
* @param {function(u2f.WrappedIosPort_)} callback |
||||
* @private |
||||
*/ |
||||
u2f.getIosPort_ = function (callback) { |
||||
setTimeout(function () { |
||||
callback(new u2f.WrappedIosPort_()); |
||||
}, 0); |
||||
}; |
||||
|
||||
/** |
||||
* A wrapper for chrome.runtime.Port that is compatible with MessagePort. |
||||
* @param {Port} port |
||||
* @constructor |
||||
* @private |
||||
*/ |
||||
u2f.WrappedChromeRuntimePort_ = function (port) { |
||||
this.port_ = port; |
||||
}; |
||||
|
||||
/** |
||||
* Format and return a sign request compliant with the JS API version supported by the extension. |
||||
* @param {Array<u2f.SignRequest>} signRequests |
||||
* @param {number} timeoutSeconds |
||||
* @param {number} reqId |
||||
* @return {Object} |
||||
*/ |
||||
u2f.formatSignRequest_ = function ( |
||||
appId, |
||||
challenge, |
||||
registeredKeys, |
||||
timeoutSeconds, |
||||
reqId |
||||
) { |
||||
if (js_api_version === undefined || js_api_version < 1.1) { |
||||
// Adapt request to the 1.0 JS API
|
||||
var signRequests = []; |
||||
for (var i = 0; i < registeredKeys.length; i++) { |
||||
signRequests[i] = { |
||||
version: registeredKeys[i].version, |
||||
challenge: challenge, |
||||
keyHandle: registeredKeys[i].keyHandle, |
||||
appId: appId, |
||||
}; |
||||
} |
||||
return { |
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST, |
||||
signRequests: signRequests, |
||||
timeoutSeconds: timeoutSeconds, |
||||
requestId: reqId, |
||||
}; |
||||
} |
||||
// JS 1.1 API
|
||||
return { |
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST, |
||||
appId: appId, |
||||
challenge: challenge, |
||||
registeredKeys: registeredKeys, |
||||
timeoutSeconds: timeoutSeconds, |
||||
requestId: reqId, |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Format and return a register request compliant with the JS API version supported by the extension.. |
||||
* @param {Array<u2f.SignRequest>} signRequests |
||||
* @param {Array<u2f.RegisterRequest>} signRequests |
||||
* @param {number} timeoutSeconds |
||||
* @param {number} reqId |
||||
* @return {Object} |
||||
*/ |
||||
u2f.formatRegisterRequest_ = function ( |
||||
appId, |
||||
registeredKeys, |
||||
registerRequests, |
||||
timeoutSeconds, |
||||
reqId |
||||
) { |
||||
if (js_api_version === undefined || js_api_version < 1.1) { |
||||
// Adapt request to the 1.0 JS API
|
||||
for (var i = 0; i < registerRequests.length; i++) { |
||||
registerRequests[i].appId = appId; |
||||
} |
||||
var signRequests = []; |
||||
for (var i = 0; i < registeredKeys.length; i++) { |
||||
signRequests[i] = { |
||||
version: registeredKeys[i].version, |
||||
challenge: registerRequests[0], |
||||
keyHandle: registeredKeys[i].keyHandle, |
||||
appId: appId, |
||||
}; |
||||
} |
||||
return { |
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST, |
||||
signRequests: signRequests, |
||||
registerRequests: registerRequests, |
||||
timeoutSeconds: timeoutSeconds, |
||||
requestId: reqId, |
||||
}; |
||||
} |
||||
// JS 1.1 API
|
||||
return { |
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST, |
||||
appId: appId, |
||||
registerRequests: registerRequests, |
||||
registeredKeys: registeredKeys, |
||||
timeoutSeconds: timeoutSeconds, |
||||
requestId: reqId, |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Posts a message on the underlying channel. |
||||
* @param {Object} message |
||||
*/ |
||||
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) { |
||||
this.port_.postMessage(message); |
||||
}; |
||||
|
||||
/** |
||||
* Emulates the HTML 5 addEventListener interface. Works only for the |
||||
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. |
||||
* @param {string} eventName |
||||
* @param {function({data: Object})} handler |
||||
*/ |
||||
u2f.WrappedChromeRuntimePort_.prototype.addEventListener = function ( |
||||
eventName, |
||||
handler |
||||
) { |
||||
var name = eventName.toLowerCase(); |
||||
if (name == "message" || name == "onmessage") { |
||||
this.port_.onMessage.addListener(function (message) { |
||||
// Emulate a minimal MessageEvent object
|
||||
handler({ data: message }); |
||||
}); |
||||
} else { |
||||
console.error("WrappedChromeRuntimePort only supports onMessage"); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Wrap the Authenticator app with a MessagePort interface. |
||||