Authentication Alpha

master
Harald Wolff 2019-11-15 13:44:44 +01:00
parent d1663cbbbe
commit f001840670
4 changed files with 668 additions and 29 deletions

View File

@ -73,6 +73,33 @@ body {
font-size: 12px;
}
.ln-view {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
display: flex;
flex-direction: column;
}
section#header {
flex-grow: 0;
margin-bottom: 1em;
background-color: whitesmoke;
}
section#body {
flex-grow: 1;
overflow: auto;
}
section#footer {
flex-grow: 0;
background-color: whitesmoke;
}
.json {
white-space: pre;
font-family: 'Courier New', Courier, monospace;
@ -153,7 +180,6 @@ div.ln-select::after {
padding-top: 4px;
padding-bottom: 4px;
margin-bottom: 2em;
}
.ln-navitem {
@ -211,6 +237,83 @@ div.ln-nav-children > .ln-navitem:first-of-type {
border-top: none;
}
div.ln-statusbar {
position: relative;
bottom: 0px;
left: 0px;
right: 0px;
border-top: 1px solid black;
padding: 8px;
display: flex;
flex-direction: row;
}
div.ln-statusbar > div {
flex-grow: 1;
text-align: center;
padding: 4px;
padding-left: 8px;
padding-right: 8px;
border-left: 1px solid #D0D0D0;
}
div.ln-statusbar > div:first-of-type {
text-align: left;
border-left: none;
}
div.ln-statusbar > div.ln-background-tasks {
flex-grow: 0;
}
.ln-background-tasks {
position: relative;
}
div.ln-background-tasks > div {
display: block;
position: absolute;
white-space: pre;
left: 4px;
bottom: 80%;
overflow-y: visible;
overflow-x: hidden;
background-color: blanchedalmond;
border: 1px solid black;
padding: 4px;
padding-right: 32px;
padding-left: 16px;
margin-right: 24px;
opacity: 0;
height: 10px;
transition: opacity 500ms 50ms, height 0ms 550ms;
}
div.ln-background-tasks:hover > div {
opacity: 1.0;
height: 8em;
transition: opacity 500ms 50ms, height 500ms 50ms;
}
div.ln-background-tasks > div > div[state="waiting"] {
color: silver;
}
div.ln-background-tasks > div > div[state="failed"] {
color: red;
}
div.ln-background-tasks > div > div[state="ready"] {
color: green;
}
div.ln-upload {
display: inline-block;
position: relative;

View File

@ -17,19 +17,32 @@
table#controls > tbody > tr > td:nth-child(3) {
padding-left: 24px;
}
div#frame {
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div id="frame">
<h1>ln.vue Demo Application</h1>
<div v-if="false">
<h2>Please wait for application to be loaded...</h2>
</div>
<ln-navbar></ln-navbar>
<router-view></router-view>
<div id="frame" class="ln-view">
<section id="header">
<h1>ln.vue Demo Application</h1>
<ln-navbar></ln-navbar>
</section>
<section id="body">
<div v-if="false">
<h2>Please wait for application to be loaded...</h2>
</div>
<div id="viewPane" class="ln-viewpane">
<router-view></router-view>
</div>
</section>
<section id="footer">
<ln-statusbar></ln-statusbar>
</section>
</div>
<script type="text/javascript">
@ -72,16 +85,6 @@
}
};
/* LNVue.addRouteTemplate(
"/state",
`<div>Current User Input object:<br><br>
<span class="json">{{ JSON.stringify(controls,null,4) }}</span>
</div>`
);
LNVue.addRoute("/controls","/controls.html");
LNVue.addRoute("/table","/table.html");
*/
let message = { value: "" };
app = new LNVue("#frame",{
@ -197,6 +200,18 @@
},
});
app.addModule({
navigation: {
login: {
label: "Login Pane",
path: "/login",
}
},
routes: {
'/login': `<div>Login Pane<br><ln-login-pane></ln-login-pane></div>`,
}
});
app.Start();
LNVue.onidle(()=>{
@ -206,6 +221,14 @@
},1000);
});
for (let n=5;n<15;n++){
LNVue.LNPromise(n + " sec promise test",(resolve,reject)=>{
setTimeout(()=>{
resolve();
},n * 1000);
});
}
</script>
</body>

View File

@ -90,6 +90,36 @@ Vue.directive('tooltip',{
},
});
Vue.component('ln-statusbar',{
computed: {
CurrentPromises: function(){
return LNVue.$_.getCurrentPromises();
}
},
template: `
<div
class="ln-statusbar"
>
<div>{{ LNVue.$_.statusText }}</div>
<div style="flex-grow: 0;">{{ LNVue.$_.socket && LNVue.$_.socket._state }}</div>
<div
class="ln-background-tasks"
>{{ Object.keys(CurrentPromises).length || "No" }} Background Tasks
<div
v-if="Object.keys(CurrentPromises).length"
>
<div
v-for="promise in CurrentPromises"
:state="promise.state"
>{{ promise.label }}</div>
</div>
</div>
<div style="flex-grow: 0;">{{ LNVue.$_.Version() }}</div>
</div>
`,
});
Vue.component('ln-navitem',{
props: {
value: {
@ -167,6 +197,18 @@ Vue.component('ln-textfield',{
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)">`,
});
Vue.component('ln-password',{
props: {
value: {
type: String,
required: true,
},
},
template: `<input
type="password"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)">`,
});
Vue.component('ln-textarea',{
props: {
@ -466,4 +508,97 @@ Vue.component('ln-select',{
});
Vue.component('ln-login-pane',{
props: {
},
methods: {
stage1: function(){
console.log("login stage1", this.identityName);
LNVue.$_
.requestChallenges(this.identityName)
.then((challenges)=>{
challenges = challenges.message.Challenges;
if (challenges.length > 0){
LNVue.$_.identity.identityName = this.identityName;
LNVue.$_.identity.challenges = challenges;
}
});
},
authenticate(challenge){
console.log("authenticate()",challenge);
},
authenticateSeededPassword(challenge){
let encoder = new TextEncoder();
let seed = LNVue.decodeHex(challenge.AuthenticationParameters);
let password = encoder.encode(challenge.prove);
console.log("password", LNVue.encodeHex(password),password);
console.log("seed", LNVue.encodeHex(seed),seed);
let secretSource = ArrayBuffer.combine(seed, password, seed);
crypto.subtle.digest("SHA-256",secretSource)
.then((secret)=>{
let challengebytes = LNVue.decodeB64(challenge.Challenge);
secret = new Uint8Array(secret);
console.log("secret", LNVue.encodeHex(secret),secret);
let proveSource = ArrayBuffer.combine(challengebytes, secret, challengebytes);
crypto.subtle.digest("SHA-256",proveSource)
.then((prove)=>{
prove = LNVue.encodeB64(new Uint8Array(prove));
LNVue.$_.authenticate(this.identityName,challenge.SecureAttributeID,challenge.Challenge,prove);
});
});
}
},
data: ()=>{
return {
identityName: "",
};
},
template: `<div>
<div
v-if="LNVue.$_.identity.uniqueID">
IdentityName: {{ LNVue.$_.identity.uniqueID }} / {{ LNVue.$_.identity.identityName }}
</div>
<div
v-if="!LNVue.$_.identity.uniqueID">
<div
v-if="LNVue.$_.identity.identityName == ''"
><label for="LNIdentityName">Username</label>
<ln-textfield
name="LNIdentityName"
v-model="identityName"></ln-textfield>
<button
:disabled="identityName == ''"
@click="stage1();"
>continue</button>
</div>
<div
v-if="LNVue.$_.identity.identityName != ''"
>
<div
v-for="challenge in LNVue.$_.identity.challenges"
>
<div
v-if="challenge.SecureAttributeTypeName == 'SeededPassword'"
>{{ challenge.SecureAttributeLabel }}
<ln-password
v-model="challenge.prove"
></ln-password>
<button
@click="authenticateSeededPassword(challenge)"
>login</button>
</div>
</div>
</div>
</div>`,
});
})();

View File

@ -1,5 +1,280 @@
var LNVue = (function (){
let _unique = new Date().getTime();
function uniqueID(){
return _unique++;
}
let currentPromises = {};
/*\
|*|
|*| Base64 / binary data / UTF-8 strings utilities (#1)
|*|
|*| https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*| Author: madmurphy
|*|
\*/
/* Array of bytes to base64 string decoding */
function b64ToUint6 (nChr) {
return nChr > 64 && nChr < 91 ?
nChr - 65
: nChr > 96 && nChr < 123 ?
nChr - 71
: nChr > 47 && nChr < 58 ?
nChr + 4
: nChr === 43 ?
62
: nChr === 47 ?
63
:
0;
}
function base64DecToArr (sBase64, nBlockSize) {
var
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);
for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
}
nUint24 = 0;
}
}
return aBytes;
}
/* Base64 string to array encoding */
function uint6ToB64 (nUint6) {
return nUint6 < 26 ?
nUint6 + 65
: nUint6 < 52 ?
nUint6 + 71
: nUint6 < 62 ?
nUint6 - 4
: nUint6 === 62 ?
43
: nUint6 === 63 ?
47
:
65;
}
function base64EncArr (aBytes) {
var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = "";
for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
/* Uncomment the following line in order to split the output in lines 76-character long: */
/*
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
*/
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
nUint24 = 0;
}
}
return eqLen === 0 ?
sB64Enc
:
sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "==");
}
class LNVuePromise
{
constructor(label,executor){
this.promise = new Promise(
(resolve,reject) => {
executor(
(v) => {
this._s.state = "ready";
resolve(v);
this.release();
},
(e) => {
this._s.state = "failed";
reject(e);
this.release();
}
);
}
);
if (!label)
label = "N.D.";
this._s = {
label,
state: "waiting"
};
this.idx = uniqueID();
currentPromises[this.idx] = this._s;
}
label(){
return this._s.label;
}
state(){
return this._s.state;
}
release(){
setTimeout(()=>{
Vue.delete(currentPromises, this.idx);
},1000);
}
then(){
return this.promise.then.apply(this.promise, arguments);
}
}
class LNVueWebSocket
{
constructor(lnvue,o){
this.LNVue = lnvue;
this.options = Object.assign({},o);
if (!this.options.url)
this.options.url = this.constructURL();
this._id = 1;
this.defaultTimeout = 30000;
this.websocket = null;
this.callbacks = {};
this._state = "initialized";
this._retry = null;
this.closing = false;
}
constructURL(){
var pageURI = window.location;
var scheme = pageURI.scheme == "https" ? "wss:" : "ws:";
var host = pageURI.host;
return scheme + "//" + host + "/socket";
}
open(){
if (this._retry){
clearTimeout(this._retry);
}
this.closing = false;
this.websocket = new WebSocket(this.options.url);
this.websocket.onopen = (e) =>{ console.log("WebSocket connected"); this._state = "ONLINE"; };
this.websocket.onclose = (e)=>{ this._onclose(e); this._state = "OFFLINE"; };
this.websocket.onerror = (e)=>{ this._onerror(e); this._state = "ERROR"; };
this.websocket.onmessage = (e)=>{ this._onmessage(e); };
return this;
}
close(){
if (this.websocket){
this.closing = true;
this.websocket.close(200,"close() called");
}
}
request(msgtype,msg,timeout){
let message = {
id: this._id++,
type: msgtype,
message: msg,
}
if (!timeout)
timeout = this.defaultTimeout;
if (timeout != -1){
return new Promise((resolve,reject)=>{
let to = setTimeout(()=>{
delete this.callbacks[message.id];
reject("timed out");
},timeout);
this.callbacks[message.id] = (msgtype,msg)=>{
clearTimeout(to);
delete this.callbacks[message.id];
if (msgtype == "error")
reject(msg);
else
resolve({type: msgtype,message: msg});
};
this.websocket.send(
JSON.stringify(message)
);
});
} else {
new Promise((resolve,reject)=>{
this.websocket.send(
JSON.stringify(message)
);
resolve();
});
}
}
_onclose(evt){
this.websocket = null;
this.options.onclose && this.options.onclose(evt);
if (!this.closing)
{
this._retry = setTimeout(() => {
this._retry = null;
console.log("reconnect...")
this.open();
}, 5000);
}
}
_onerror(evt){
this.options.onerror && this.options.onerror(evt);
}
_onmessage(evt){
try
{
let j = JSON.parse(evt.data);
let cb = this.callbacks[ j.id ];
cb && cb(j.type,j.message);
} catch(exc){
console.log(exc,evt.data);
}
}
}
class LNVue
{
@ -13,17 +288,25 @@
this.data = Object.assign({}, options.data, { LNVue: this, msg: "Hello World" });
this.promises = [];
this.globals = {
currentPromises,
};
this.statusText = "LNVue preparing";
Vue.prototype.$LNVue = this;
LNVue.$_ = this;
console.log("LNVue: preparing");
this.navigation = {};
this.identity = {
uniqueID: null,
identityName: "",
};
Promise
.all(LNVue.promises)
.then(()=>{
console.log("LNVue: starting");
this.status("LNVue: starting");
LNVue.vueRouter.addRoutes([{
path: "*",
@ -31,7 +314,9 @@
template: `<h2>404 Not Found</h2>The URL you tried to reach is not existing.`,
},
}]);
},
(cause)=>{
this.status("LNVue: start failed: " + cause);
});
this.vue = null;
@ -47,11 +332,31 @@
data: this.data,
router: LNVue.vueRouter,
});
});
});
});
LNVue.onidle(()=>{
this.socket = new LNVueWebSocket(this);
this.socket.open();
});
}
Version(){ return "0.2alpha"; };
getCurrentPromises() {
return this.globals.currentPromises;
}
status(){
if (arguments.length == 1){
this.statusText = arguments[0];
return this;
} else if (arguments.length == 0){
return this.statusText;
} else
throw "LNVue.status(): too many arguments";
}
addModule(modSpec){
if (modSpec.navigation instanceof Object){
@ -61,13 +366,16 @@
LNVue.$each(modSpec.routes,(key,route)=>{
if ((route instanceof Object) && route.url)
{
let p = new Promise((resolve,reject)=>{
let p = LNVue.LNPromise( "addModule()", (resolve,reject)=>{
LNVue
.fetch(route.url)
.then((src)=>{
this.addRoute(key,{ template: src, data: ()=>{ return this.data; }, });
resolve();
});
},
(cause)=>{
console.log("loading route.url failed: ",cause);
});
});
this.promises.push(p);
} else if (route instanceof Object){
@ -85,6 +393,45 @@
]);
}
/* Authentication API */
requestChallenges(identityName,secureAttributeTypeName){
return new Promise((resolve,reject)=>{
this.socket.request("AuthenticationRequest",{
IdentityName: identityName,
SecureAttributeTypeName: secureAttributeTypeName,
})
.then((challenges)=>{
console.log("rx challenges",challenges);
resolve(challenges);
},
(error)=>{
console.log("Login challenges could not be retrieved", error);
}
);
});
}
authenticate(identityName,secureAttributeID,challenge,prove){
let authenticationProve = {
IdentityName: identityName,
SecureAttributeUniqueID: secureAttributeID,
Challenge: challenge,
Prove: prove,
};
console.log("authenticate", authenticationProve);
this.socket.request("AuthenticationProve", authenticationProve)
.then((identity)=>{
console.log("auth",identity);
},
(error)=>{
console.log("auth error",error);
});
}
}
Object.defineProperty( LNVue, '$LNVue', {
@ -155,8 +502,7 @@
LNVue.promises = [];
LNVue.fetch = function(url,cb){
let self = this;
return new Promise(function(resolve,reject){
return LNVue.LNPromise("loading",(resolve,reject)=>{
fetch(url)
.then((response => {
if (response.status.toString().startsWith("2"))
@ -182,5 +528,37 @@
});
};
LNVue.LNPromise = function(label, action){
console.log(label,action);
return new LNVuePromise(label, action);
}
LNVue.encodeB64 = base64EncArr;
LNVue.decodeB64 = base64DecToArr;
LNVue.encodeHex = (bytes) => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
LNVue.decodeHex = (hexString) => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
ArrayBuffer.combine = function(...args){
let byteLength = 0;
args.forEach((arg,index)=>{
byteLength = byteLength + arg.byteLength;
});
let result = new Uint8Array(byteLength);
let p = 0;
args.forEach((arg,index)=>{
for (let n=0;n<arg.byteLength;n++)
{
result[p++] = arg[n];
}
});
console.log("combine",new Uint8Array(result));
return result.buffer
};
LNVue.prototypes = {};
LNVue.prototypes.LNVuePromise = LNVuePromise;
return LNVue;
})();