diff --git a/.gitignore b/.gitignore index cf5927687..e1897e4cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,13 +3,17 @@ node_modules/ *.swp .tern-port npm-debug.log -config/account-template -config/email-templates +/config/account-template +/config/email-templates /accounts /profile /inbox /.acl /config.json +/config/templates +/config/views /settings +/.db .nyc_output coverage +/data \ No newline at end of file diff --git a/bin/lib/options.js b/bin/lib/options.js index 9a47fdb33..1e882581c 100644 --- a/bin/lib/options.js +++ b/bin/lib/options.js @@ -10,9 +10,9 @@ module.exports = [ // }, { name: 'root', - help: "Root folder to serve (defaut: './')", + help: "Root folder to serve (default: './data')", question: 'Path to the folder you want to serve. Default is', - default: './', + default: './data', prompt: true, filter: (value) => path.resolve(value) }, @@ -46,18 +46,30 @@ module.exports = [ default: '/', prompt: true }, + { + name: 'config-path', + question: 'Path to the config directory (for example: /etc/solid-server)', + default: './config', + prompt: true + }, + { + name: 'db-path', + question: 'Path to the server metadata db directory (for users/apps etc)', + default: './.db', + prompt: true + }, { name: 'auth', help: 'Pick an authentication strategy for WebID: `tls` or `oidc`', question: 'Select authentication strategy', type: 'list', choices: [ - 'WebID-TLS' + 'WebID-OpenID Connect' ], prompt: false, - default: 'WebID-TLS', + default: 'WebID-OpenID Connect', filter: (value) => { - if (value === 'WebID-TLS') return 'tls' + if (value === 'WebID-OpenID Connect') return 'oidc' }, when: (answers) => { return answers.webid @@ -132,7 +144,6 @@ module.exports = [ default: '/proxy', prompt: true }, - { name: 'file-browser', help: 'Type the URL of default app to use for browsing files (or use default)', diff --git a/common/css/bootstrap.min.css b/common/css/bootstrap.min.css new file mode 100644 index 000000000..ed3905e0e --- /dev/null +++ b/common/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/config/defaults.js b/config/defaults.js index 79b39c9e3..b67c66c81 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -1,7 +1,14 @@ 'use strict' module.exports = { - 'AUTH_METHOD': 'tls', - 'DEFAULT_PORT': 8443, - 'DEFAULT_URI': 'https://localhost:8443' // default serverUri + 'auth': 'oidc', + 'localAuth': { + 'tls': true, + 'password': true + }, + 'configPath': './config', + 'dbPath': './.db', + 'port': 8443, + 'serverUri': 'https://localhost:8443', + 'webid': true } diff --git a/test/resources/acl/empty-acl/.acl b/data/.gitkeep similarity index 100% rename from test/resources/acl/empty-acl/.acl rename to data/.gitkeep diff --git a/default-templates/emails/reset-password.js b/default-templates/emails/reset-password.js new file mode 100644 index 000000000..fb18972cc --- /dev/null +++ b/default-templates/emails/reset-password.js @@ -0,0 +1,49 @@ +'use strict' + +/** + * Returns a partial Email object (minus the `to` and `from` properties), + * suitable for sending with Nodemailer. + * + * Used to send a Reset Password email, upon user request + * + * @param data {Object} + * + * @param data.resetUrl {string} + * @param data.webId {string} + * + * @return {Object} + */ +function render (data) { + return { + subject: 'Account password reset', + + /** + * Text version + */ + text: `Hi, + +We received a request to reset your password for your Solid account, ${data.webId} + +To reset your password, click on the following link: + +${data.resetUrl} + +If you did not mean to reset your password, ignore this email, your password will not change.`, + + /** + * HTML version + */ + html: `

Hi,

+ +

We received a request to reset your password for your Solid account, ${data.webId}

+ +

To reset your password, click on the following link:

+ +

${data.resetUrl}

+ +

If you did not mean to reset your password, ignore this email, your password will not change.

+` + } +} + +module.exports.render = render diff --git a/default-email-templates/welcome.js b/default-templates/emails/welcome.js similarity index 95% rename from default-email-templates/welcome.js rename to default-templates/emails/welcome.js index 21ca3ba61..bce554462 100644 --- a/default-email-templates/welcome.js +++ b/default-templates/emails/welcome.js @@ -14,7 +14,7 @@ */ function render (data) { return { - subject: `Welcome to Solid`, + subject: 'Welcome to Solid', /** * Text version of the Welcome email diff --git a/default-account-template/.acl b/default-templates/new-account/.acl similarity index 100% rename from default-account-template/.acl rename to default-templates/new-account/.acl diff --git a/default-account-template/.meta b/default-templates/new-account/.meta similarity index 100% rename from default-account-template/.meta rename to default-templates/new-account/.meta diff --git a/default-account-template/.meta.acl b/default-templates/new-account/.meta.acl similarity index 100% rename from default-account-template/.meta.acl rename to default-templates/new-account/.meta.acl diff --git a/default-account-template/favicon.ico b/default-templates/new-account/favicon.ico similarity index 100% rename from default-account-template/favicon.ico rename to default-templates/new-account/favicon.ico diff --git a/default-account-template/favicon.ico.acl b/default-templates/new-account/favicon.ico.acl similarity index 100% rename from default-account-template/favicon.ico.acl rename to default-templates/new-account/favicon.ico.acl diff --git a/default-account-template/inbox/.acl b/default-templates/new-account/inbox/.acl similarity index 100% rename from default-account-template/inbox/.acl rename to default-templates/new-account/inbox/.acl diff --git a/default-templates/new-account/index.html b/default-templates/new-account/index.html new file mode 100644 index 000000000..6c5abd03c --- /dev/null +++ b/default-templates/new-account/index.html @@ -0,0 +1,28 @@ + + + + + + Solid User Profile + + + +
+

Solid User Profile

+
+
+
+
+

+ Welcome to your Solid user profile. +

+

+ Your Web ID is:
+ + {{webId}} +

+
+
+
+ + diff --git a/default-templates/new-account/index.html.acl b/default-templates/new-account/index.html.acl new file mode 100644 index 000000000..47c7640a2 --- /dev/null +++ b/default-templates/new-account/index.html.acl @@ -0,0 +1,22 @@ +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo ; + + acl:mode + acl:Read, acl:Write, acl:Control. + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./index.html>; + + acl:mode acl:Read. diff --git a/default-account-template/profile/card b/default-templates/new-account/profile/card similarity index 100% rename from default-account-template/profile/card rename to default-templates/new-account/profile/card diff --git a/default-account-template/profile/card.acl b/default-templates/new-account/profile/card.acl similarity index 100% rename from default-account-template/profile/card.acl rename to default-templates/new-account/profile/card.acl diff --git a/default-account-template/settings/.acl b/default-templates/new-account/settings/.acl similarity index 100% rename from default-account-template/settings/.acl rename to default-templates/new-account/settings/.acl diff --git a/default-account-template/settings/prefs.ttl b/default-templates/new-account/settings/prefs.ttl similarity index 100% rename from default-account-template/settings/prefs.ttl rename to default-templates/new-account/settings/prefs.ttl diff --git a/default-account-template/settings/privateTypeIndex.ttl b/default-templates/new-account/settings/privateTypeIndex.ttl similarity index 100% rename from default-account-template/settings/privateTypeIndex.ttl rename to default-templates/new-account/settings/privateTypeIndex.ttl diff --git a/default-account-template/settings/publicTypeIndex.ttl b/default-templates/new-account/settings/publicTypeIndex.ttl similarity index 100% rename from default-account-template/settings/publicTypeIndex.ttl rename to default-templates/new-account/settings/publicTypeIndex.ttl diff --git a/default-account-template/settings/publicTypeIndex.ttl.acl b/default-templates/new-account/settings/publicTypeIndex.ttl.acl similarity index 100% rename from default-account-template/settings/publicTypeIndex.ttl.acl rename to default-templates/new-account/settings/publicTypeIndex.ttl.acl diff --git a/default-templates/server/index.html b/default-templates/server/index.html new file mode 100644 index 000000000..6101fdcb7 --- /dev/null +++ b/default-templates/server/index.html @@ -0,0 +1,35 @@ + + + + + + Welcome to Solid + + + +
+

Welcome to Solid

+
+
+
+
+

+ If you have not already done so, please create an account. +

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + diff --git a/default-templates/server/index.html.acl b/default-templates/server/index.html.acl new file mode 100644 index 000000000..de9032975 --- /dev/null +++ b/default-templates/server/index.html.acl @@ -0,0 +1,11 @@ +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./index.html>; + + acl:mode acl:Read. diff --git a/default-views/account/register-disabled.hbs b/default-views/account/register-disabled.hbs new file mode 100644 index 000000000..4a84e3660 --- /dev/null +++ b/default-views/account/register-disabled.hbs @@ -0,0 +1,4 @@ +

+ Registering a new account is disabled for the WebID-TLS authentication method. + Please restart the server using another mode. +

diff --git a/default-views/account/register-form.hbs b/default-views/account/register-form.hbs new file mode 100644 index 000000000..d0d721022 --- /dev/null +++ b/default-views/account/register-form.hbs @@ -0,0 +1,55 @@ +
+
+ {{#if error}} +
+
+

{{error}}

+
+
+ {{/if}} +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+ +
+
+
+ + + {{> auth/auth-hidden-fields}} +
+ +
+
Already have an account? + + Log In + +
+
+
+
+
diff --git a/default-views/account/register.hbs b/default-views/account/register.hbs new file mode 100644 index 000000000..12ab66a9f --- /dev/null +++ b/default-views/account/register.hbs @@ -0,0 +1,21 @@ + + + + + + Register + + + +
+

Register

+
+
+ {{#if registerDisabled}} + {{> account/register-disabled}} + {{else}} + {{> account/register-form}} + {{/if}} +
+ + diff --git a/default-views/auth/auth-hidden-fields.hbs b/default-views/auth/auth-hidden-fields.hbs new file mode 100644 index 000000000..ddfe82507 --- /dev/null +++ b/default-views/auth/auth-hidden-fields.hbs @@ -0,0 +1,7 @@ + + + + + + + diff --git a/default-views/auth/change-password.hbs b/default-views/auth/change-password.hbs new file mode 100644 index 000000000..88a0d8292 --- /dev/null +++ b/default-views/auth/change-password.hbs @@ -0,0 +1,65 @@ + + + + + + Change Password + + + +
+

Change Password

+
+
+
+ {{#if error}} +
+
+
+

{{error}}

+
+
+
+ {{/if}} + + {{#if validToken}} +
+
+
+ + + +
+
+
+ +
+
+
+ +
+
+ + + +
+ {{else}} + + {{/if}} +
+
+ + diff --git a/default-views/auth/consent.hbs b/default-views/auth/consent.hbs new file mode 100644 index 000000000..615aa74b0 --- /dev/null +++ b/default-views/auth/consent.hbs @@ -0,0 +1,33 @@ + + + + + + {{title}} + + + + + +
+

Authorize app to use your Web ID?

+
+
+
+ + + + + + + + + + +
+
+ + diff --git a/default-views/auth/goodbye.hbs b/default-views/auth/goodbye.hbs new file mode 100644 index 000000000..305cccac0 --- /dev/null +++ b/default-views/auth/goodbye.hbs @@ -0,0 +1,20 @@ + + + + + + Logged Out + + + +
+

You have logged out.

+
+
+
+ +
+
+ + diff --git a/default-views/auth/login-tls.hbs b/default-views/auth/login-tls.hbs new file mode 100644 index 000000000..6a98e3c6c --- /dev/null +++ b/default-views/auth/login-tls.hbs @@ -0,0 +1,10 @@ +
+
+ + + + {{> auth/auth-hidden-fields}} +
+
diff --git a/default-views/auth/login-username-password.hbs b/default-views/auth/login-username-password.hbs new file mode 100644 index 000000000..19ba04a29 --- /dev/null +++ b/default-views/auth/login-username-password.hbs @@ -0,0 +1,23 @@ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + + + {{> auth/auth-hidden-fields}} +
+
diff --git a/default-views/auth/login.hbs b/default-views/auth/login.hbs new file mode 100644 index 000000000..e4bf37a65 --- /dev/null +++ b/default-views/auth/login.hbs @@ -0,0 +1,58 @@ + + + + + + Login + + + +
+

Login

+
+
+ {{#if error}} +
+
+

{{error}}

+
+
+ {{/if}} +
+
+ {{#if enablePassword}} + {{> auth/login-username-password}} + {{/if}} +
+

+
+ {{#if enableTls}} + {{> auth/login-tls}} + {{/if}} +
+
+
+ +
+
+
+
Don't have an account? + + Register + +
+
+ +
+
Forgot password? + + Reset password + +
+
+
+
+ + diff --git a/default-views/auth/password-changed.hbs b/default-views/auth/password-changed.hbs new file mode 100644 index 000000000..4522cf9ed --- /dev/null +++ b/default-views/auth/password-changed.hbs @@ -0,0 +1,23 @@ + + + + + + Password Changed + + + +
+

Password Changed

+
+
+

Your password has been changed.

+ +

+ + Log in + +

+
+ + diff --git a/default-views/auth/reset-link-sent.hbs b/default-views/auth/reset-link-sent.hbs new file mode 100644 index 000000000..059727515 --- /dev/null +++ b/default-views/auth/reset-link-sent.hbs @@ -0,0 +1,17 @@ + + + + + + Reset Link Sent + + + +
+

Reset Link Sent

+
+
+ A Reset Password link has been sent to your email. +
+ + diff --git a/default-views/auth/reset-password.hbs b/default-views/auth/reset-password.hbs new file mode 100644 index 000000000..8821171ad --- /dev/null +++ b/default-views/auth/reset-password.hbs @@ -0,0 +1,59 @@ + + + + + + Reset Password + + + +
+

Reset Password

+
+
+
+
+ {{#if error}} +
+
+

{{error}}

+
+
+ {{/if}} +
+
+ {{#if multiUser}} +

Please enter your account name. A password reset link will be + emailed to the address you provided during account registration.

+ + + + {{else}} +

A password reset link will be + emailed to the address you provided during account registration.

+ {{/if}} +
+
+
+ +
+
+
+ +
+ +
+
Don't have an account? + Register +
+
+
+ + +
+
+
+ + diff --git a/default-views/auth/select-provider.hbs b/default-views/auth/select-provider.hbs new file mode 100644 index 000000000..2c7fa1382 --- /dev/null +++ b/default-views/auth/select-provider.hbs @@ -0,0 +1,27 @@ + + + + + + Select Provider + + + +
+
+

Select Provider

+
+
+
+
+
+ + + +
+ +
+
+ + diff --git a/lib/account-recovery.js b/lib/account-recovery.js deleted file mode 100644 index 6821c809b..000000000 --- a/lib/account-recovery.js +++ /dev/null @@ -1,115 +0,0 @@ -module.exports = AccountRecovery - -const express = require('express') -const TokenService = require('./token-service') -const bodyParser = require('body-parser') -const path = require('path') -const debug = require('debug')('solid:account-recovery') -const utils = require('./utils') -const sym = require('rdflib').sym -const url = require('url') - -function AccountRecovery (options = {}) { - const router = express.Router('/') - const tokenService = new TokenService() - const generateEmail = function (host, account, email, token) { - return { - from: '"Account Recovery" ', - to: email, - subject: 'Recover your account', - text: 'Hello,\n' + - 'You asked to retrieve your account: ' + account + '\n' + - 'Copy this address in your browser addressbar:\n\n' + - 'https://' + path.join(host, '/api/accounts/validateToken?token=' + token) // TODO find a way to get the full url - // html: '' - } - } - - router.get('/recover', function (req, res, next) { - res.set('Content-Type', 'text/html') - res.sendFile(path.join(__dirname, '../static/account-recovery.html')) - }) - - router.post('/recover', bodyParser.urlencoded({ extended: false }), function (req, res, next) { - debug('getting request for account recovery', req.body.webid) - const ldp = req.app.locals.ldp - const emailService = req.app.locals.emailService - const baseUri = utils.uriAbs(req) - - // if (!req.body.webid) { - // res.status(406).send('You need to pass an account') - // return - // } - - // Check if account exists - let webid = url.parse(req.body.webid) - let hostname = webid.hostname - - ldp.graph(hostname, '/' + ldp.suffixAcl, baseUri, function (err, graph) { - if (err) { - debug('cannot find graph of the user', req.body.webid || ldp.root, err) - res.status(err.status || 500).send('Fail to find user') - return - } - - // TODO do a query - let emailAddress - graph - .statementsMatching(undefined, sym('http://www.w3.org/ns/auth/acl#agent')) - .some(function (statement) { - if (statement.object.uri.startsWith('mailto:')) { - emailAddress = statement.object.uri - return true - } - }) - - if (!emailAddress) { - res.status(406).send('No emailAddress registered in your account') - return - } - - const token = tokenService.generate({ webid: req.body.webid }) - const email = generateEmail(req.get('host'), req.body.webid, emailAddress, token) - emailService.sendMail(email, function (err, info) { - if (err) { - res.send(500, 'Failed to send the email for account recovery, try again') - return - } - - res.send('Requested') - }) - }) - }) - - router.get('/validateToken', function (req, res, next) { - if (!req.query.token) { - res.status(406).send('Token is required') - return - } - - const tokenContent = tokenService.verify(req.query.token) - - if (!tokenContent) { - debug('token was not found', tokenContent) - res.status(401).send('Token not valid') - return - } - - if (tokenContent && !tokenContent.webid) { - debug('token does not match account', tokenContent) - res.status(401).send('Token not valid') - return - } - - debug('token was valid', tokenContent) - - tokenService.remove(req.query.token) - - req.session.userId = tokenContent.webid // TODO add the full path - req.session.identified = true - res.set('User', tokenContent.webid) - res.redirect(options.redirect) - }) - - return router -} diff --git a/lib/api/accounts/user-accounts.js b/lib/api/accounts/user-accounts.js index aee3ea202..e3f704e8f 100644 --- a/lib/api/accounts/user-accounts.js +++ b/lib/api/accounts/user-accounts.js @@ -18,6 +18,7 @@ const AddCertificateRequest = require('../../requests/add-cert-request') function checkAccountExists (accountManager) { return (req, res, next) => { let accountUri = req.hostname + accountManager.accountUriExists(accountUri) .then(found => { if (!found) { @@ -31,61 +32,6 @@ function checkAccountExists (accountManager) { } } -/** - * Returns an Express middleware handler for creating a new user account - * (POST /api/accounts/new). - * - * @param accountManager {AccountManager} - * - * @return {Function} - */ -function createAccount (accountManager) { - return (req, res, next) => { - let request - - try { - request = CreateAccountRequest.fromParams(req, res, accountManager) - } catch (err) { - err.status = err.status || 400 - return next(err) - } - - return request.createAccount() - .catch(err => { - err.status = err.status || 400 - next(err) - }) - } -} - -/** - * Returns an Express middleware handler for intercepting any GET requests - * for first time users (in single user mode), and redirecting them to the - * signup page. - * - * @param accountManager {AccountManager} - * - * @return {Function} - */ -function firstTimeSignupRedirect (accountManager) { - return (req, res, next) => { - // Only redirect browser (HTML) requests to first-time signup - if (!req.accepts('text/html')) { return next() } - - accountManager.accountExists() - .then(found => { - if (!found) { - debug('(single user mode) Redirecting to account creation') - - res.redirect(302, '/signup.html') - } else { - next() - } - }) - .catch(next) - } -} - /** * Returns an Express middleware handler for adding a new certificate to an * existing account (POST to /api/accounts/cert). @@ -115,24 +61,10 @@ function newCertificate (accountManager) { function middleware (accountManager) { let router = express.Router('/') - if (accountManager.multiUser) { - router.get('/', checkAccountExists(accountManager)) - } else { - // In single user mode, if account has not yet been created, intercept - // all GET requests and redirect to the Signup form - accountManager.accountExists() - .then(found => { - if (!found) { - router.use('/signup.html', express.static('./static/signup.html')) - router.get('/*', firstTimeSignupRedirect(accountManager)) - } - }) - .catch(error => { - debug('Error during accountExists(): ', error) - }) - } + router.get('/', checkAccountExists(accountManager)) - router.post('/api/accounts/new', bodyParser, createAccount(accountManager)) + router.post('/api/accounts/new', bodyParser, CreateAccountRequest.post) + router.get(['/register', '/api/accounts/new'], CreateAccountRequest.get) router.post('/api/accounts/cert', bodyParser, newCertificate(accountManager)) @@ -142,7 +74,5 @@ function middleware (accountManager) { module.exports = { middleware, checkAccountExists, - createAccount, - firstTimeSignupRedirect, newCertificate } diff --git a/lib/api/authn/index.js b/lib/api/authn/index.js index d3474e2da..9caf979ea 100644 --- a/lib/api/authn/index.js +++ b/lib/api/authn/index.js @@ -1,6 +1,35 @@ 'use strict' +const debug = require('../../debug').authentication + +/** + * Enforces the `--force-user` server flag, hardcoding a webid for all requests, + * for testing purposes. + */ +function overrideWith (forceUserId) { + return (req, res, next) => { + req.session.userId = forceUserId + req.session.identified = true + debug('Identified user (override): ' + forceUserId) + res.set('User', forceUserId) + return next() + } +} + +/** + * Sets the `User:` response header if the user has been authenticated. + */ +function setUserHeader (req, res, next) { + let session = req.session + let webId = session.identified && session.userId + + res.set('User', webId || '') + next() +} + module.exports = { - signin: require('./signin'), - signout: require('./signout') + oidc: require('./webid-oidc'), + tls: require('./webid-tls'), + overrideWith, + setUserHeader } diff --git a/lib/api/authn/signin.js b/lib/api/authn/signin.js deleted file mode 100644 index 01e88709f..000000000 --- a/lib/api/authn/signin.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = signin - -const validUrl = require('valid-url') -const request = require('request') -const li = require('li') - -function signin () { - return (req, res, next) => { - if (!validUrl.isUri(req.body.webid)) { - return res.status(400).send('This is not a valid URI') - } - - request({ method: 'OPTIONS', uri: req.body.webid }, function (err, req) { - if (err) { - res.status(400).send('Did not find a valid endpoint') - return - } - if (!req.headers.link) { - res.status(400).send('The URI requested is not a valid endpoint') - return - } - - const linkHeaders = li.parse(req.headers.link) - console.log(linkHeaders) - if (!linkHeaders['oidc.issuer']) { - res.status(400).send('The URI requested is not a valid endpoint') - return - } - - res.redirect(linkHeaders['oidc.issuer']) - }) - } -} diff --git a/lib/api/authn/signout.js b/lib/api/authn/signout.js deleted file mode 100644 index 16ab7372c..000000000 --- a/lib/api/authn/signout.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = signout - -function signout () { - return (req, res, next) => { - req.session.userId = '' - req.session.identified = false - res.status(200).send() - } -} diff --git a/lib/api/authn/webid-oidc.js b/lib/api/authn/webid-oidc.js new file mode 100644 index 000000000..90ffc65c6 --- /dev/null +++ b/lib/api/authn/webid-oidc.js @@ -0,0 +1,144 @@ +'use strict' +/** + * OIDC Relying Party API handler module. + */ + +const express = require('express') +const bodyParser = require('body-parser').urlencoded({ extended: false }) + +const { LoginRequest } = require('../../requests/login-request') + +const PasswordResetEmailRequest = require('../../requests/password-reset-email-request') +const PasswordChangeRequest = require('../../requests/password-change-request') + +const { + AuthCallbackRequest, + LogoutRequest, + SelectProviderRequest +} = require('oidc-auth-manager').handlers + +/** + * Returns a router with OIDC Relying Party and Identity Provider middleware: + * + * @method middleware + * + * @param oidc {OidcManager} + * + * @return {Router} Express router + */ +function middleware (oidc) { + const router = express.Router('/') + + // User-facing Authentication API + router.get('/api/auth/select-provider', SelectProviderRequest.get) + router.post('/api/auth/select-provider', bodyParser, SelectProviderRequest.post) + + router.get(['/login', '/signin'], LoginRequest.get) + + router.post('/login/password', bodyParser, LoginRequest.loginPassword) + + router.post('/login/tls', bodyParser, LoginRequest.loginTls) + + router.get('/account/password/reset', PasswordResetEmailRequest.get) + router.post('/account/password/reset', bodyParser, PasswordResetEmailRequest.post) + + router.get('/account/password/change', PasswordChangeRequest.get) + router.post('/account/password/change', bodyParser, PasswordChangeRequest.post) + + router.get('/logout', LogoutRequest.handle) + + router.get('/goodbye', (req, res) => { res.render('auth/goodbye') }) + + // The relying party callback is called at the end of the OIDC signin process + router.get('/api/oidc/rp/:issuer_id', AuthCallbackRequest.get) + + // Initialize the OIDC Identity Provider routes/api + // router.get('/.well-known/openid-configuration', discover.bind(provider)) + // router.get('/jwks', jwks.bind(provider)) + // router.post('/register', register.bind(provider)) + // router.get('/authorize', authorize.bind(provider)) + // router.post('/authorize', authorize.bind(provider)) + // router.post('/token', token.bind(provider)) + // router.get('/userinfo', userinfo.bind(provider)) + // router.get('/logout', logout.bind(provider)) + let oidcProviderApi = require('oidc-op-express')(oidc.provider) + router.use('/', oidcProviderApi) + + return router +} + +/** + * Sets the `WWW-Authenticate` response header for 401 error responses. + * Used by error-pages handler. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * @param err {Error} + */ +function setAuthenticateHeader (req, res, err) { + let locals = req.app.locals + + let errorParams = { + realm: locals.host.serverUri, + scope: 'openid', + error: err.error, + error_description: err.error_description, + error_uri: err.error_uri + } + + let challengeParams = Object.keys(errorParams) + .filter(key => !!errorParams[key]) + .map(key => `${key}="${errorParams[key]}"`) + .join(', ') + + res.set('WWW-Authenticate', 'Bearer ' + challengeParams) +} + +/** + * Provides custom logic for error status code overrides. + * + * @param statusCode {number} + * @param req {IncomingRequest} + * + * @returns {number} + */ +function statusCodeOverride (statusCode, req) { + if (isEmptyToken(req)) { + return 400 + } else { + return statusCode + } +} + +/** + * Tests whether the `Authorization:` header includes an empty or missing Bearer + * token. + * + * @param req {IncomingRequest} + * + * @returns {boolean} + */ +function isEmptyToken (req) { + let header = req.get('Authorization') + + if (!header) { return false } + + if (header.startsWith('Bearer')) { + let fragments = header.split(' ') + + if (fragments.length === 1) { + return true + } else if (!fragments[1]) { + return true + } + } + + return false +} + +module.exports = { + isEmptyToken, + middleware, + setAuthenticateHeader, + statusCodeOverride +} diff --git a/lib/api/authn/webid-tls.js b/lib/api/authn/webid-tls.js new file mode 100644 index 000000000..c63012407 --- /dev/null +++ b/lib/api/authn/webid-tls.js @@ -0,0 +1,62 @@ +var webid = require('webid/tls') +var debug = require('../../debug').authentication + +function authenticate () { + return handler +} + +function handler (req, res, next) { + // User already logged in? skip + if (req.session.userId && req.session.identified) { + debug('User: ' + req.session.userId) + res.set('User', req.session.userId) + return next() + } + + var certificate = req.connection.getPeerCertificate() + // Certificate is empty? skip + if (certificate === null || Object.keys(certificate).length === 0) { + debug('No client certificate found in the request. Did the user click on a cert?') + setEmptySession(req) + return next() + } + + // Verify webid + webid.verify(certificate, function (err, result) { + if (err) { + debug('Error processing certificate: ' + err.message) + setEmptySession(req) + return next() + } + req.session.userId = result + req.session.identified = true + debug('Identified user: ' + req.session.userId) + res.set('User', req.session.userId) + return next() + }) +} + +function setEmptySession (req) { + req.session.userId = '' + req.session.identified = false +} + +/** + * Sets the `WWW-Authenticate` response header for 401 error responses. + * Used by error-pages handler. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + */ +function setAuthenticateHeader (req, res) { + let locals = req.app.locals + + res.set('WWW-Authenticate', `WebID-TLS realm="${locals.host.serverUri}"`) +} + +module.exports = { + authenticate, + handler, + setAuthenticateHeader, + setEmptySession +} diff --git a/lib/api/index.js b/lib/api/index.js index 9e6b2a80b..5ce7a3514 100644 --- a/lib/api/index.js +++ b/lib/api/index.js @@ -2,6 +2,7 @@ module.exports = { authn: require('./authn'), - messages: require('./messages'), + oidc: require('./authn/webid-oidc'), + tls: require('./authn/webid-tls'), accounts: require('./accounts/user-accounts') } diff --git a/lib/api/messages/index.js b/lib/api/messages/index.js deleted file mode 100644 index 79ddf69a6..000000000 --- a/lib/api/messages/index.js +++ /dev/null @@ -1,104 +0,0 @@ -exports.send = send - -const error = require('../../http-error') -const debug = require('debug')('solid:api:messages') -const utils = require('../../utils') -const sym = require('rdflib').sym -const url = require('url') -const waterfall = require('run-waterfall') - -function send () { - return (req, res, next) => { - if (!req.session.userId) { - next(error(401, 'You need to be authenticated')) - return - } - - if (!req.body.message || req.body.message.length < 0) { - next(error(406, 'You need to specify a message')) - return - } - - if (!req.body.to) { - next(error(406, 'You need to specify a the destination')) - return - } - - if (req.body.to.split(':').length !== 2) { - next(error(406, 'Destination badly formatted')) - return - } - - waterfall([ - (cb) => getLoggedUserName(req, cb), - (displayName, cb) => { - const vars = { - me: displayName, - message: req.body.message - } - - if (req.body.to.split(':') === 'mailto' && req.app.locals.emailService) { - sendEmail(req, vars, cb) - } else { - cb(error(406, 'Messaging service not available')) - } - } - ], (err) => { - if (err) { - next(err) - return - } - - res.send('message sent') - }) - } -} - -function getLoggedUserName (req, callback) { - const ldp = req.app.locals.ldp - const baseUri = utils.uriAbs(req) - const webid = url.parse(req.session.userId) - - ldp.graph(webid.hostname, '/' + webid.pathname, baseUri, function (err, graph) { - if (err) { - debug('cannot find graph of the user', req.session.userId || ldp.root, err) - // TODO for now only users of this IDP can send emails - callback(error(403, 'Your user cannot perform this operation')) - return - } - - // TODO do a query - let displayName - graph - .statementsMatching(undefined, sym('http://xmlns.com/foaf/0.1/name')) - .some(function (statement) { - if (statement.object.value) { - displayName = statement.object.value - return true - } - }) - - if (!displayName) { - displayName = webid.hostname - } - callback(null, displayName) - }) -} - -function sendEmail (req, vars, callback) { - const emailService = req.app.locals.emailService - const emailData = { - from: 'no-reply@' + webid.hostname, - to: req.body.to.split(':')[1] - } - const webid = url.parse(req.session.userId) - - emailService.messageTemplate((template) => { - var send = emailService.mailer.templateSender( - template, - { from: emailData.from }) - - // use template based sender to send a message - send({ to: emailData.to }, vars, callback) - }) -} diff --git a/lib/capability-discovery.js b/lib/capability-discovery.js index 39b204982..058988512 100644 --- a/lib/capability-discovery.js +++ b/lib/capability-discovery.js @@ -10,10 +10,12 @@ const serviceConfigDefaults = { 'accounts': { // 'changePassword': '/api/account/changePassword', // 'delete': '/api/accounts/delete', + + // Create new user (see IdentityProvider.post() in identity-provider.js) 'new': '/api/accounts/new', 'recover': '/api/accounts/recover', - 'signin': '/api/accounts/signin', - 'signout': '/api/accounts/signout', + 'signin': '/login', + 'signout': '/logout', 'validateToken': '/api/accounts/validateToken' } } @@ -43,7 +45,7 @@ function capabilityDiscovery () { * @param next */ function serviceCapabilityDocument (serviceConfig) { - return (req, res, next) => { + return (req, res) => { // Add the server root url serviceConfig.root = util.uriBase(req) // TODO make sure we align with the rest // Add the 'apps' urls section diff --git a/lib/create-app.js b/lib/create-app.js index ec4039b19..50a06fdab 100644 --- a/lib/create-app.js +++ b/lib/create-app.js @@ -2,6 +2,7 @@ module.exports = createApp const express = require('express') const session = require('express-session') +const handlebars = require('express-handlebars') const uuid = require('uuid') const cors = require('cors') const LDP = require('./ldp') @@ -10,21 +11,21 @@ const proxy = require('./handlers/proxy') const SolidHost = require('./models/solid-host') const AccountManager = require('./models/account-manager') const vhost = require('vhost') -const fs = require('fs-extra') -const path = require('path') const EmailService = require('./models/email-service') -const AccountRecovery = require('./account-recovery') +const TokenService = require('./models/token-service') const capabilityDiscovery = require('./capability-discovery') -const bodyParser = require('body-parser').urlencoded({ extended: false }) const API = require('./api') -const authentication = require('./handlers/authentication') const errorPages = require('./handlers/error-pages') +const OidcManager = require('./models/oidc-manager') +const config = require('./server-config') +const defaults = require('../config/defaults') +const options = require('./handlers/options') -var corsSettings = cors({ +const corsSettings = cors({ methods: [ 'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE' ], - exposedHeaders: 'User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, Content-Length', + exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, Content-Length, WWW-Authenticate', credentials: true, maxAge: 1728000, origin: true, @@ -32,135 +33,181 @@ var corsSettings = cors({ }) function createApp (argv = {}) { - argv.host = SolidHost.from({ port: argv.port, serverUri: argv.serverUri }) - argv.templates = initTemplates() + // Override default configs (defaults) with passed-in params (argv) + argv = Object.assign({}, defaults, argv) - let ldp = new LDP(argv) - let app = express() + argv.host = SolidHost.from({ port: argv.port, serverUri: argv.serverUri }) - app.use(corsSettings) + const configPath = config.initConfigDir(argv) + argv.templates = config.initTemplateDirs(configPath) - app.options('*', (req, res, next) => { - res.status(204) - next() - }) + const ldp = new LDP(argv) - // Setting options as local variable - app.locals.ldp = ldp - app.locals.appUrls = argv.apps // used for service capability discovery - let multiUser = argv.idp - - if (argv.email && argv.email.host) { - app.locals.emailService = new EmailService(argv.templates.email, argv.email) - } - - // Set X-Powered-By - app.use(function (req, res, next) { - res.set('X-Powered-By', 'solid-server') - next() - }) - - // Set default Allow methods - app.use(function (req, res, next) { - res.set('Allow', 'OPTIONS, HEAD, GET, PATCH, POST, PUT, DELETE') - next() - }) + const app = express() - app.use('/', capabilityDiscovery()) + initAppLocals(app, argv, ldp) + initHeaders(app) + initViews(app, configPath) - // Use session cookies - let useSecureCookies = argv.webid // argv.webid forces https and secure cookies - app.use(session(sessionSettings(useSecureCookies, argv.host))) + // Serve the public 'common' directory (for shared CSS files, etc) + app.use('/common', express.static('common')) // Adding proxy - if (ldp.proxy) { - proxy(app, ldp.proxy) + if (argv.proxy) { + proxy(app, argv.proxy) } - if (ldp.webid) { - var accountRecovery = AccountRecovery({ redirect: '/' }) - // adds GET /api/accounts/recover - // adds POST /api/accounts/recover - // adds GET /api/accounts/validateToken - app.use('/api/accounts/', accountRecovery) - - let accountManager = AccountManager.from({ - authMethod: argv.auth, - emailService: app.locals.emailService, - host: argv.host, - accountTemplatePath: argv.templates.account, - store: ldp, - multiUser - }) - - // Account Management API (create account, new cert) - app.use('/', API.accounts.middleware(accountManager)) - - // Authentication API (login/logout) - app.post('/api/accounts/signin', bodyParser, API.authn.signin()) - app.post('/api/accounts/signout', API.authn.signout()) - - // Messaging API - app.post('/api/messages', authentication, bodyParser, API.messages.send()) - } + // Options handler + app.options('/*', options) if (argv.apiApps) { app.use('/api/apps', express.static(argv.apiApps)) } - if (ldp.idp) { - app.use(vhost('*', LdpMiddleware(corsSettings))) + if (argv.webid) { + initWebId(argv, app, ldp) } app.use('/', LdpMiddleware(corsSettings)) // Errors - app.use(errorPages) + app.use(errorPages.handler) return app } -function initTemplates () { - let accountTemplatePath = ensureTemplateCopiedTo( - '../default-account-template', - '../config/account-template' - ) - - let emailTemplatesPath = ensureTemplateCopiedTo( - '../default-email-templates', - '../config/email-templates' - ) +/** + * Initializes `app.locals` parameters for downstream use (typically by route + * handlers). + * + * @param app {Function} Express.js app instance + * @param argv {Object} Config options hashmap + * @param ldp {LDP} + */ +function initAppLocals (app, argv, ldp) { + app.locals.ldp = ldp + app.locals.appUrls = argv.apps // used for service capability discovery + app.locals.host = argv.host + app.locals.authMethod = argv.auth + app.locals.localAuth = argv.localAuth + app.locals.tokenService = new TokenService() - return { - account: accountTemplatePath, - email: emailTemplatesPath + if (argv.email && argv.email.host) { + app.locals.emailService = new EmailService(argv.templates.email, argv.email) } } /** - * Ensures that a template directory has been initialized in `config/` from - * default templates. + * Sets up headers common to all Solid requests (CORS-related, Allow, etc). * - * @param defaultTemplateDir {string} Path to a default template directory, - * relative to `lib/`. For example, '../default-email-templates' contains - * various email templates pre-defined by the Solid dev team. + * @param app {Function} Express.js app instance + */ +function initHeaders (app) { + app.use(corsSettings) + + app.use((req, res, next) => { + // Set X-Powered-By + res.set('X-Powered-By', 'solid-server') + // Set default Allow methods + res.set('Allow', 'OPTIONS, HEAD, GET, PATCH, POST, PUT, DELETE') + next() + }) + + app.use('/', capabilityDiscovery()) +} + +/** + * Sets up the express rendering engine and views directory. * - * @param configTemplateDir {string} Path to a template directory customized - * to this particular installation (relative to `lib/`). Server operators - * are encouraged to override/customize these templates in the `config/` - * directory. + * @param app {Function} Express.js app + * @param configPath {string} + */ +function initViews (app, configPath) { + const viewsPath = config.initDefaultViews(configPath) + + app.set('views', viewsPath) + app.engine('.hbs', handlebars({ + extname: '.hbs', + partialsDir: viewsPath + })) + app.set('view engine', '.hbs') +} + +/** + * Sets up WebID-related functionality (account creation and authentication) * - * @return {string} Returns the absolute path to the customizable template copy + * @param argv {Object} + * @param app {Function} + * @param ldp {LDP} */ -function ensureTemplateCopiedTo (defaultTemplateDir, configTemplateDir) { - let configTemplatePath = path.join(__dirname, configTemplateDir) - let defaultTemplatePath = path.join(__dirname, defaultTemplateDir) +function initWebId (argv, app, ldp) { + config.ensureWelcomePage(argv) + + // Use session cookies + const useSecureCookies = argv.webid // argv.webid forces https and secure cookies + app.use(session(sessionSettings(useSecureCookies, argv.host))) + + let accountManager = AccountManager.from({ + authMethod: argv.auth, + emailService: app.locals.emailService, + tokenService: app.locals.tokenService, + host: argv.host, + accountTemplatePath: argv.templates.account, + store: ldp, + multiUser: argv.idp + }) + app.locals.accountManager = accountManager - if (!fs.existsSync(configTemplatePath)) { - fs.copySync(defaultTemplatePath, configTemplatePath) + // Account Management API (create account, new cert) + app.use('/', API.accounts.middleware(accountManager)) + + // Set up authentication-related API endpoints and app.locals + initAuthentication(argv, app) + + if (argv.idp) { + app.use(vhost('*', LdpMiddleware(corsSettings))) + } +} + +/** + * Sets up authentication-related routes and handlers for the app. + * + * @param argv {Object} Config options hashmap + * @param app {Function} Express.js app instance + */ +function initAuthentication (argv, app) { + let authMethod = argv.auth + + if (argv.forceUser) { + app.use('/', API.authn.overrideWith(argv.forceUser)) + return } - return configTemplatePath + switch (authMethod) { + case 'tls': + // Enforce authentication with WebID-TLS on all LDP routes + app.use('/', API.tls.authenticate()) + break + case 'oidc': + let oidc = OidcManager.fromServerConfig(argv) + app.locals.oidc = oidc + + oidc.initialize() + + // Initialize the WebId-OIDC authentication routes/api, including: + // user-facing Solid endpoints (/login, /logout, /api/auth/select-provider) + // and OIDC-specific ones + app.use('/', API.oidc.middleware(oidc)) + + // Enforce authentication with WebID-OIDC on all LDP routes + app.use('/', oidc.rs.authenticate()) + + app.use('/', API.authn.setUserHeader) + + break + default: + throw new TypeError('Unsupported authentication scheme') + } } /** diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index 3b8c47e96..5487d909a 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,4 +1,7 @@ -module.exports.allow = allow +module.exports = { + allow, + userIdFromRequest +} var ACL = require('../acl-checker') var $rdf = require('rdflib') @@ -93,8 +96,30 @@ function fetchDocument (host, ldp, baseUri) { } } +/** + * Extracts the Web ID from the request object (for purposes of access control). + * + * @param req {IncomingRequest} + * + * @return {string|null} Web ID + */ +function userIdFromRequest (req) { + let userId + let locals = req.app.locals + + if (req.session.userId) { + userId = req.session.userId + } else if (locals.authMethod === 'oidc') { + userId = locals.oidc.webIdFromClaims(req.claims) + } + + return userId +} + function getUserId (req, callback) { - callback(null, req.session.userId) + let userId = userIdFromRequest(req) + + callback(null, userId) // var onBehalfOf = req.get('On-Behalf-Of') // if (!onBehalfOf) { // return callback(null, req.session.userId) diff --git a/lib/handlers/authentication.js b/lib/handlers/authentication.js deleted file mode 100644 index fef1df026..000000000 --- a/lib/handlers/authentication.js +++ /dev/null @@ -1,64 +0,0 @@ -module.exports = handler - -var webid = require('webid/tls') -var debug = require('../debug').authentication -var error = require('../http-error') - -function handler (req, res, next) { - var ldp = req.app.locals.ldp - - if (ldp.forceUser) { - req.session.userId = ldp.forceUser - req.session.identified = true - debug('Identified user: ' + req.session.userId) - res.set('User', req.session.userId) - return next() - } - - // No webid required? skip - if (!ldp.webid) { - setEmptySession(req) - return next() - } - - // User already logged in? skip - if (req.session.userId && req.session.identified) { - debug('User: ' + req.session.userId) - res.set('User', req.session.userId) - return next() - } - - if (ldp.auth === 'tls') { - var certificate = req.connection.getPeerCertificate() - // Certificate is empty? skip - if (certificate === null || Object.keys(certificate).length === 0) { - debug('No client certificate found in the request. Did the user click on a cert?') - setEmptySession(req) - return next() - } - - // Verify webid - webid.verify(certificate, function (err, result) { - if (err) { - debug('Error processing certificate: ' + err.message) - setEmptySession(req) - return next() - } - req.session.userId = result - req.session.identified = true - debug('Identified user: ' + req.session.userId) - res.set('User', req.session.userId) - return next() - }) - } else if (ldp.auth === 'oidc') { - setEmptySession(req) - return next() - } else { - return next(error(500, 'Authentication method not supported')) - } -} - -function setEmptySession (req) { - req.session.userId = '' - req.session.identified = false -} diff --git a/lib/handlers/error-pages.js b/lib/handlers/error-pages.js index 18a5f3bc4..f099caad3 100644 --- a/lib/handlers/error-pages.js +++ b/lib/handlers/error-pages.js @@ -1,38 +1,209 @@ -module.exports = handler +const debug = require('../debug').server +const fs = require('fs') +const util = require('../utils') +const Auth = require('../api/authn') -var debug = require('../debug').server -var fs = require('fs') +// Authentication methods that require a Provider Select page +const SELECT_PROVIDER_AUTH_METHODS = ['oidc'] +/** + * Serves as a last-stop error handler for all other middleware. + * + * @param err {Error} + * @param req {IncomingRequest} + * @param res {ServerResponse} + * @param next {Function} + */ function handler (err, req, res, next) { debug('Error page because of ' + err.message) - var ldp = req.app.locals.ldp + let locals = req.app.locals + let authMethod = locals.authMethod + let ldp = locals.ldp - // If the user specifies this function - // then, they can customize the error programmatically + // If the user specifies this function, + // they can customize the error programmatically if (ldp.errorHandler) { + debug('Using custom error handler') return ldp.errorHandler(err, req, res, next) } + let statusCode = statusCodeFor(err, req, authMethod) + + if (statusCode === 401) { + setAuthenticateHeader(req, res, err) + } + + if (requiresSelectProvider(authMethod, statusCode, req)) { + return redirectToSelectProvider(req, res) + } + // If noErrorPages is set, - // then use built-in express default error handler + // then return the response directly if (ldp.noErrorPages) { - return res - .status(err.status) - .send(err.message + '\n' || '') + sendErrorResponse(statusCode, res, err) + } else { + sendErrorPage(statusCode, res, err, ldp) + } +} + +/** + * Returns the HTTP status code for a given request error. + * + * @param err {Error} + * @param req {IncomingRequest} + * @param authMethod {string} + * + * @returns {number} + */ +function statusCodeFor (err, req, authMethod) { + let statusCode = err.status || err.statusCode || 500 + + if (authMethod === 'oidc') { + statusCode = Auth.oidc.statusCodeOverride(statusCode, req) } - // Check if error page exists - var errorPage = ldp.errorPages + err.status.toString() + '.html' - fs.readFile(errorPage, 'utf8', function (readErr, text) { - if (readErr) { - return res - .status(err.status) - .send(err.message || '') - } - - res.status(err.status) - res.header('Content-Type', 'text/html') - res.send(text) + return statusCode +} + +/** + * Tests whether a given authentication method requires a Select Provider + * page redirect for 401 error responses. + * + * @param authMethod {string} + * @param statusCode {number} + * @param req {IncomingRequest} + * + * @returns {boolean} + */ +function requiresSelectProvider (authMethod, statusCode, req) { + if (statusCode !== 401) { return false } + + if (!SELECT_PROVIDER_AUTH_METHODS.includes(authMethod)) { return false } + + if (!req.accepts('text/html')) { return false } + + return true +} + +/** + * Dispatches the writing of the `WWW-Authenticate` response header (used for + * 401 Unauthorized responses). + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * @param err {Error} + */ +function setAuthenticateHeader (req, res, err) { + let locals = req.app.locals + let authMethod = locals.authMethod + + switch (authMethod) { + case 'oidc': + Auth.oidc.setAuthenticateHeader(req, res, err) + break + case 'tls': + Auth.tls.setAuthenticateHeader(req, res) + break + default: + break + } +} + +/** + * Sends the HTTP status code and error message in the response. + * + * @param statusCode {number} + * @param res {ServerResponse} + * @param err {Error} + */ +function sendErrorResponse (statusCode, res, err) { + res.status(statusCode) + res.send(err.message + '\n') +} + +/** + * Sends the HTTP status code and error message as a custom error page. + * + * @param statusCode {number} + * @param res {ServerResponse} + * @param err {Error} + * @param ldp {LDP} + */ +function sendErrorPage (statusCode, res, err, ldp) { + let errorPage = ldp.errorPages + statusCode.toString() + '.html' + + return new Promise((resolve) => { + fs.readFile(errorPage, 'utf8', (readErr, text) => { + if (readErr) { + // Fall back on plain error response + return resolve(sendErrorResponse(statusCode, res, err)) + } + + res.status(statusCode) + res.header('Content-Type', 'text/html') + res.send(text) + resolve() + }) }) } + +/** + * Sends a 401 response with an HTML http-equiv type redirect body, to + * redirect any users requesting a resource directly in the browser to the + * Select Provider page and login workflow. + * Implemented as a 401 + redirect body instead of a 302 to provide a useful + * 401 response to REST/XHR clients. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + */ +function redirectToSelectProvider (req, res) { + res.status(401) + res.header('Content-Type', 'text/html') + + let currentUrl = util.fullUrlForReq(req) + req.session.returnToUrl = currentUrl + + let locals = req.app.locals + let loginUrl = locals.host.serverUri + + '/api/auth/select-provider?returnToUrl=' + currentUrl + debug('Redirecting to Select Provider: ' + loginUrl) + + let body = redirectBody(loginUrl) + res.send(body) +} + +/** + * Returns a response body for redirecting browsers to a Select Provider / + * login workflow page. Uses either a JS location.href redirect or an + * http-equiv type html redirect for no-script conditions. + * + * @param url {string} + * + * @returns {string} Response body + */ +function redirectBody (url) { + return ` + + + +Redirecting... +If you are not redirected automatically, +follow the link to login +` +} + +module.exports = { + handler, + redirectBody, + redirectToSelectProvider, + requiresSelectProvider, + sendErrorPage, + sendErrorResponse, + setAuthenticateHeader +} diff --git a/lib/handlers/options.js b/lib/handlers/options.js index 792782136..20b19eab3 100644 --- a/lib/handlers/options.js +++ b/lib/handlers/options.js @@ -4,8 +4,28 @@ const utils = require('../utils') module.exports = handler function handler (req, res, next) { + linkServiceEndpoint(req, res) + linkAuthProvider(req, res) + linkSparqlEndpoint(res) + + res.status(204) + + next() +} + +function linkAuthProvider (req, res) { + let locals = req.app.locals + if (locals.authMethod === 'oidc') { + let oidcProviderUri = locals.host.serverUri + addLink(res, oidcProviderUri, 'oidc.provider') + } +} + +function linkServiceEndpoint (req, res) { let serviceEndpoint = `${utils.uriBase(req)}/.well-known/solid` addLink(res, serviceEndpoint, 'service') +} + +function linkSparqlEndpoint (res) { res.header('Accept-Patch', 'application/sparql-update') - next() } diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index 24b10e6e7..a8d45ff12 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -1,198 +1,109 @@ -module.exports = handler +// Express handler for LDP PATCH requests -var mime = require('mime-types') -var fs = require('fs') -var $rdf = require('rdflib') -var debug = require('../debug').handlers -var utils = require('../utils.js') -var error = require('../http-error') -const waterfall = require('run-waterfall') +module.exports = handler -const DEFAULT_CONTENT_TYPE = 'text/turtle' +const bodyParser = require('body-parser') +const mime = require('mime-types') +const fs = require('fs') +const debug = require('../debug').handlers +const utils = require('../utils.js') +const error = require('../http-error') +const $rdf = require('rdflib') -function handler (req, res, next) { - req.setEncoding('utf8') - req.text = '' - req.on('data', function (chunk) { - req.text += chunk - }) +const DEFAULT_TARGET_TYPE = 'text/turtle' - req.on('end', function () { - patchHandler(req, res, next) - }) +// Patch handlers by request body content type +const PATCHERS = { + 'application/sparql-update': require('./patch/sparql-update-patcher.js') } +// Handles a PATCH request function patchHandler (req, res, next) { - var ldp = req.app.locals.ldp debug('PATCH -- ' + req.originalUrl) - debug('PATCH -- text length: ' + (req.text ? req.text.length : 'undefined2')) res.header('MS-Author-Via', 'SPARQL') - var root = !ldp.idp ? ldp.root : ldp.root + req.hostname + '/' - var filename = utils.uriToFilename(req.path, root) - var targetContentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - var patchContentType = req.get('content-type') - ? req.get('content-type').split(';')[0].trim() // Ignore parameters - : '' - var targetURI = utils.uriAbs(req) + req.originalUrl + // Obtain details of the patch document + const patch = { + text: req.body ? req.body.toString() : '', + contentType: (req.get('content-type') || '').match(/^[^;\s]*/)[0] + } + const patchGraph = PATCHERS[patch.contentType] + if (!patchGraph) { + return next(error(415, 'Unknown patch content type: ' + patch.contentType)) + } + debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType) + + // Obtain details of the target resource + const ldp = req.app.locals.ldp + const root = !ldp.idp ? ldp.root : ldp.root + req.hostname + '/' + const target = { + file: utils.uriToFilename(req.path, root), + uri: utils.uriAbs(req) + req.originalUrl + } + target.contentType = mime.lookup(target.file) || DEFAULT_TARGET_TYPE + debug('PATCH -- Target <%s> (%s)', target.uri, target.contentType) + + // Read the RDF graph to be patched from the file + readGraph(target) + // Patch the graph and write it back to the file + .then(graph => patchGraph(graph, target.uri, patch.text)) + .then(graph => writeGraph(graph, target)) + // Send the result to the client + .then(result => { res.send(result) }) + .then(next, next) +} - debug('PATCH -- Content-type ' + patchContentType + ' patching target ' + targetContentType + ' <' + targetURI + '>') +// Reads the request body and calls the actual patch handler +function handler (req, res, next) { + readEntity(req, res, () => patchHandler(req, res, next)) +} +const readEntity = bodyParser.text({ type: () => true }) - if (patchContentType === 'application/sparql') { - sparql(filename, targetURI, req.text, function (err, result) { - if (err) { - return next(err) - } - res.json(result) - return next() - }) - } else if (patchContentType === 'application/sparql-update') { - return sparqlUpdate(filename, targetURI, req.text, function (err, patchKB) { +// Reads the RDF graph in the given resource +function readGraph (resource) { + // Read the resource's file + return new Promise((resolve, reject) => + fs.readFile(resource.file, {encoding: 'utf8'}, function (err, fileContents) { if (err) { - return next(err) + // If the file does not exist, assume empty contents + // (it will be created after a successful patch) + if (err.code === 'ENOENT') { + fileContents = '' + // Fail on all other errors + } else { + return reject(error(500, 'Patch: Original file read error:' + err)) + } } - - // subscription.publishDelta(req, res, patchKB, targetURI) - debug('PATCH -- applied OK (sync)') - res.send('Patch applied OK\n') - return next() + debug('PATCH -- Read target file (%d bytes)', fileContents.length) + resolve(fileContents) }) - } else { - return next(error(400, 'Unknown patch content type: ' + patchContentType)) - } -} // postOrPatch - -function sparql (filename, targetURI, text, callback) { - debug('PATCH -- parsing query ...') - var patchURI = targetURI // @@@ beware the triples from the patch ending up in the same place - var patchKB = $rdf.graph() - var targetKB = $rdf.graph() - var targetContentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - var query = $rdf.SPARQLToQuery(text, false, patchKB, patchURI) // last param not used ATM - - fs.readFile(filename, {encoding: 'utf8'}, function (err, dataIn) { - if (err) { - return callback(error(404, 'Patch: Original file read error:' + err)) - } - - debug('PATCH -- File read OK ' + dataIn.length) - debug('PATCH -- parsing target file ...') - + ) + // Parse the resource's file contents + .then((fileContents) => { + const graph = $rdf.graph() + debug('PATCH -- Reading %s with content type %s', resource.uri, resource.contentType) try { - $rdf.parse(dataIn, targetKB, targetURI, targetContentType) - } catch (e) { - debug('Patch: Target ' + targetContentType + ' file syntax error:' + e) - return callback(error(500, 'Patch: Target ' + targetContentType + ' file syntax error:' + e)) - } - debug('PATCH -- Target parsed OK ') - - var bindingsArray = [] - - var onBindings = function (bindings) { - var b = {} - var v - var x - for (v in bindings) { - if (bindings.hasOwnProperty(v)) { - x = bindings[v] - b[v] = x.uri ? {'type': 'uri', 'value': x.uri} : { 'type': 'literal', 'value': x.value } - if (x.lang) { - b[v]['xml:lang'] = x.lang - } - if (x.dt) { - b[v].dt = x.dt.uri // @@@ Correct? @@ check - } - } - } - debug('PATCH -- bindings: ' + JSON.stringify(b)) - bindingsArray.push(b) - } - - var onDone = function () { - debug('PATCH -- Query done, no. bindings: ' + bindingsArray.length) - return callback(null, { - 'head': { - 'vars': query.vars.map(function (v) { - return v.toNT() - }) - }, - 'results': { - 'bindings': bindingsArray - } - }) + $rdf.parse(fileContents, graph, resource.uri, resource.contentType) + } catch (err) { + throw error(500, 'Patch: Target ' + resource.contentType + ' file syntax error:' + err) } - - var fetcher = new $rdf.Fetcher(targetKB, 10000, true) - targetKB.query(query, onBindings, fetcher, onDone) + debug('PATCH -- Parsed target file') + return graph }) } -function sparqlUpdate (filename, targetURI, text, callback) { - var patchURI = targetURI // @@@ beware the triples from the patch ending up in the same place - var patchKB = $rdf.graph() - var targetKB = $rdf.graph() - var targetContentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - - debug('PATCH -- parsing patch ...') - var patchObject - try { - // Must parse relative to document's base address but patch doc should get diff URI - patchObject = $rdf.sparqlUpdateParser(text, patchKB, patchURI) - } catch (e) { - return callback(error(400, 'Patch format syntax error:\n' + e + '\n')) - } - debug('PATCH -- reading target file ...') - - waterfall([ - (cb) => { - fs.stat(filename, (err) => { - if (!err) return cb() - - fs.writeFile(filename, '', (err) => { - if (err) { - return cb(error(err, 'Error creating the patch target')) - } - cb() - }) - }) - }, - (cb) => { - fs.readFile(filename, {encoding: 'utf8'}, function (err, dataIn) { - if (err) { - return cb(error(500, 'Error reading the patch target')) - } - - debug('PATCH -- target read OK ' + dataIn.length + ' bytes. Parsing...') - - try { - $rdf.parse(dataIn, targetKB, targetURI, targetContentType) - } catch (e) { - debug('Patch: Target ' + targetContentType + ' file syntax error:' + e) - return cb(error(500, 'Patch: Target ' + targetContentType + ' file syntax error:' + e)) - } - - var target = patchKB.sym(targetURI) - debug('PATCH -- Target parsed OK, patching... ') - - targetKB.applyPatch(patchObject, target, function (err) { - if (err) { - var message = err.message || err // returns string at the moment - debug('PATCH FAILED. Returning 409. Message: \'' + message + '\'') - return cb(error(409, 'Error when applying the patch')) - } - debug('PATCH -- Patched. Writeback URI base ' + targetURI) - var data = $rdf.serialize(target, targetKB, targetURI, targetContentType) - // debug('Writeback data: ' + data) +// Writes the RDF graph to the given resource +function writeGraph (graph, resource) { + return new Promise((resolve, reject) => { + const resourceSym = graph.sym(resource.uri) + const serialized = $rdf.serialize(resourceSym, graph, resource.uri, resource.contentType) - fs.writeFile(filename, data, {encoding: 'utf8'}, function (err, data) { - if (err) { - return cb(error(500, 'Failed to write file back after patch: ' + err)) - } - debug('PATCH -- applied OK (sync)') - return cb(null, patchKB) - }) - }) - }) - } - ], callback) + fs.writeFile(resource.file, serialized, {encoding: 'utf8'}, function (err) { + if (err) { + return reject(error(500, 'Failed to write file back after patch: ' + err)) + } + debug('PATCH -- applied OK (sync)') + resolve('Patch applied OK\n') + }) + }) } diff --git a/lib/handlers/patch/sparql-update-patcher.js b/lib/handlers/patch/sparql-update-patcher.js new file mode 100644 index 000000000..b10f227b6 --- /dev/null +++ b/lib/handlers/patch/sparql-update-patcher.js @@ -0,0 +1,36 @@ +// Performs an application/sparql-update patch on a graph + +module.exports = patch + +const $rdf = require('rdflib') +const debug = require('../../debug').handlers +const error = require('../../http-error') + +// Patches the given graph +function patch (targetKB, targetURI, patchText) { + return new Promise((resolve, reject) => { + // Parse the patch document + debug('PATCH -- Parsing patch...') + const patchURI = targetURI // @@@ beware the triples from the patch ending up in the same place + const patchKB = $rdf.graph() + var patchObject + try { + // Must parse relative to document's base address but patch doc should get diff URI + patchObject = $rdf.sparqlUpdateParser(patchText, patchKB, patchURI) + } catch (e) { + return reject(error(400, 'Patch format syntax error:\n' + e + '\n')) + } + debug('PATCH -- reading target file ...') + + // Apply the patch to the target graph + var target = patchKB.sym(targetURI) + targetKB.applyPatch(patchObject, target, function (err) { + if (err) { + var message = err.message || err // returns string at the moment + debug('PATCH FAILED. Returning 409. Message: \'' + message + '\'') + return reject(error(409, 'Error when applying the patch')) + } + resolve(targetKB) + }) + }) +} diff --git a/lib/handlers/proxy.js b/lib/handlers/proxy.js index e4049e9a4..d7893840d 100644 --- a/lib/handlers/proxy.js +++ b/lib/handlers/proxy.js @@ -15,7 +15,7 @@ function addProxy (app, path) { path, cors({ methods: ['GET'], - exposedHeaders: 'User, Location, Link, Vary, Last-Modified, Content-Length', + exposedHeaders: 'Authorization, User, Location, Link, Vary, Last-Modified, Content-Length', maxAge: 1728000, origin: true }), diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 6ad5ff271..6de76305e 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -1,22 +1,34 @@ module.exports = handler var debug = require('debug')('solid:put') +const header = require('../header') function handler (req, res, next) { var ldp = req.app.locals.ldp debug(req.originalUrl) res.header('MS-Author-Via', 'SPARQL') - ldp.put(req.hostname, req.path, req, function (err) { + const linkHeader = header.parseMetadataFromHeader(req.get('Link')) + let resourcePath = req.path + + const isContainer = resourcePath.endsWith('/') || + linkHeader.isContainer || linkHeader.isBasicContainer + + // Normalize container path if necessary + if (isContainer && !resourcePath.endsWith('/')) { + resourcePath += '/' + } + + ldp.put(req.hostname, resourcePath, req, function (err, status) { if (err) { debug('error putting the file:' + err.message) err.message = 'Can\'t write file: ' + err.message return next(err) } - debug('succeded putting the file') + debug('succeeded putting the file') - res.sendStatus(201) + res.sendStatus(status) return next() }) } diff --git a/lib/ldp-middleware.js b/lib/ldp-middleware.js index 88dd6475c..dfcbf3e1b 100644 --- a/lib/ldp-middleware.js +++ b/lib/ldp-middleware.js @@ -3,12 +3,10 @@ module.exports = LdpMiddleware var express = require('express') var header = require('./header') var acl = require('./handlers/allow') -var authentication = require('./handlers/authentication') var get = require('./handlers/get') var post = require('./handlers/post') var put = require('./handlers/put') var del = require('./handlers/delete') -var options = require('./handlers/options') var patch = require('./handlers/patch') var index = require('./handlers/index') var copy = require('./handlers/copy') @@ -19,26 +17,16 @@ function LdpMiddleware (corsSettings) { // Add Link headers router.use(header.linksHandler) - // TODO edit cors - // router.use((req, res, next) => { - // edit cors according to ACL - // }) if (corsSettings) { router.use(corsSettings) } - router.use('/*', authentication) router.copy('/*', acl.allow('Write'), copy) router.get('/*', index, acl.allow('Read'), get) router.post('/*', acl.allow('Append'), post) router.patch('/*', acl.allow('Write'), patch) router.put('/*', acl.allow('Write'), put) router.delete('/*', acl.allow('Write'), del) - router.options('/*', options) - - // TODO: in the process of being deprecated - // Convert json-ld and nquads to turtle - // router.use('/*', parse.parseHandler) return router } diff --git a/lib/ldp.js b/lib/ldp.js index 58a44962d..37d323585 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -75,14 +75,14 @@ class LDP { this.skin = true } - if (this.webid && !this.auth) { - this.auth = 'tls' - } - if (this.proxy && this.proxy[ 0 ] !== '/') { this.proxy = '/' + this.proxy } + debug.settings('Server URI: ' + this.serverUri) + debug.settings('Auth method: ' + this.auth) + debug.settings('Db path: ' + this.dbPath) + debug.settings('Config path: ' + this.configPath) debug.settings('Suffix Acl: ' + this.suffixAcl) debug.settings('Suffix Meta: ' + this.suffixMeta) debug.settings('Filesystem Root: ' + this.root) @@ -266,11 +266,15 @@ class LDP { var root = !ldp.idp ? ldp.root : ldp.root + host + '/' var filePath = utils.uriToFilename(resourcePath, root, host) - // PUT requests not supported on containers. Use POST instead - if (filePath.endsWith('/')) { - return callback(error(409, - 'PUT not supported on containers, use POST instead')) + const statusCode = fs.existsSync(filePath) ? 204 : 201 + + const isContainer = filePath.endsWith('/') + + if (isContainer) { + // For container operations, request body gets written to .meta + filePath = path.join(filePath, ldp.suffixMeta) } + // First, create the enclosing directory, if necessary var dirName = path.dirname(filePath) mkdirp(dirName, (err) => { @@ -279,6 +283,7 @@ class LDP { return callback(error(err, 'Failed to create the path to the new resource')) } + // Directory created, now write the file var file = stream.pipe(fs.createWriteStream(filePath)) file.on('error', function () { @@ -286,7 +291,7 @@ class LDP { }) file.on('finish', function () { debug.handlers('PUT -- Wrote data to: ' + filePath) - callback(null) + callback(null, statusCode) }) }) } diff --git a/lib/models/account-manager.js b/lib/models/account-manager.js index 5bbcc4f68..8a6268668 100644 --- a/lib/models/account-manager.js +++ b/lib/models/account-manager.js @@ -11,6 +11,7 @@ const AccountTemplate = require('./account-template') const debug = require('./../debug').accounts const DEFAULT_PROFILE_CONTENT_TYPE = 'text/turtle' +const DEFAULT_ADMIN_USERNAME = 'admin' /** * Manages account creation (determining whether accounts exist, creating @@ -22,8 +23,9 @@ class AccountManager { /** * @constructor * @param [options={}] {Object} - * @param [options.authMethod] {string} Primary authentication method (e.g. 'tls') + * @param [options.authMethod] {string} Primary authentication method (e.g. 'oidc') * @param [options.emailService] {EmailService} + * @param [options.tokenService] {TokenService} * @param [options.host] {SolidHost} * @param [options.multiUser=false] {boolean} (argv.idp) Is the server running * in multiUser mode (users can sign up for accounts) or single user @@ -41,12 +43,13 @@ class AccountManager { } this.host = options.host this.emailService = options.emailService - this.authMethod = options.authMethod || defaults.AUTH_METHOD + this.tokenService = options.tokenService + this.authMethod = options.authMethod || defaults.auth this.multiUser = options.multiUser || false this.store = options.store this.pathCard = options.pathCard || 'profile/card' this.suffixURI = options.suffixURI || '#me' - this.accountTemplatePath = options.accountTemplatePath || './default-account-template/' + this.accountTemplatePath = options.accountTemplatePath || './default-templates/new-account/' } /** @@ -190,7 +193,8 @@ class AccountManager { * @param [accountName] {string} * * @throws {Error} via accountUriFor() - * @return {string} + * + * @return {string|null} */ accountWebIdFor (accountName) { let accountUri = this.accountUriFor(accountName) @@ -200,6 +204,22 @@ class AccountManager { return webIdUri.format() } + /** + * Returns the root .acl URI for a given user account (the account recovery + * email is stored there). + * + * @param userAccount {UserAccount} + * + * @throws {Error} via accountUriFor() + * + * @return {string} Root .acl URI + */ + rootAclFor (userAccount) { + let accountUri = this.accountUriFor(userAccount.username) + + return url.resolve(accountUri, this.store.suffixAcl) + } + /** * Adds a newly generated WebID-TLS certificate to the user's profile graph. * @@ -209,6 +229,10 @@ class AccountManager { * @return {Promise} */ addCertKeyToProfile (certificate, userAccount) { + if (!certificate) { + throw new TypeError('Cannot add empty certificate to user profile') + } + return this.getProfileGraphFor(userAccount) .then(profileGraph => { return this.addCertKeyToGraph(certificate, profileGraph) @@ -302,7 +326,14 @@ class AccountManager { * Creates and returns a `UserAccount` instance from submitted user data * (typically something like `req.body`, from a signup form). * - * @param userData {Object} Options hashmap, like `req.body` + * @param userData {Object} Options hashmap, like `req.body`. + * Either a `username` or a `webid` property is required. + * + * @param [userData.username] {string} + * @param [uesrData.webid] {string} + * + * @param [userData.email] {string} + * @param [userData.name] {string} * * @throws {Error} (via `accountWebIdFor()`) If in multiUser mode and no * username passed @@ -313,12 +344,33 @@ class AccountManager { let userConfig = { username: userData.username, email: userData.email, - name: userData.name + name: userData.name, + webId: userData.webid || userData.webId || + this.accountWebIdFor(userData.username) } - userConfig.webId = userData.webid || this.accountWebIdFor(userData.username) + + if (userConfig.webId && !userConfig.username) { + userConfig.username = this.usernameFromWebId(userConfig.webId) + } + + if (!userConfig.webId && !userConfig.username) { + throw new Error('Username or web id is required') + } + return UserAccount.from(userConfig) } + usernameFromWebId (webId) { + if (!this.multiUser) { + return DEFAULT_ADMIN_USERNAME + } + + let profileUrl = url.parse(webId) + let hostname = profileUrl.hostname + + return hostname.split('.')[0] + } + /** * Creates a user account storage folder (from a default account template). * @@ -340,6 +392,121 @@ class AccountManager { }) } + externalAccount (webId) { + let webIdHostname = url.parse(webId).hostname + + let serverHostname = this.host.hostname + + return !webIdHostname.endsWith(serverHostname) + } + + /** + * Generates an expiring one-time-use token for password reset purposes + * (the user's Web ID is saved in the token service). + * + * @param userAccount {UserAccount} + * + * @return {string} Generated token + */ + generateResetToken (userAccount) { + return this.tokenService.generate({ webId: userAccount.webId }) + } + + /** + * Validates that a token exists and is not expired, and returns the saved + * token contents, or throws an error if invalid. + * Does not consume / clear the token. + * + * @param token {string} + * + * @throws {Error} If missing or invalid token + * + * @return {Object|false} Saved token data object if verified, false otherwise + */ + validateResetToken (token) { + let tokenValue = this.tokenService.verify(token) + + if (!tokenValue) { + throw new Error('Invalid or expired reset token') + } + + return tokenValue + } + + /** + * Returns a password reset URL (to be emailed to the user upon request) + * + * @param token {string} One-time-use expiring token, via the TokenService + * @param returnToUrl {string} + * + * @return {string} + */ + passwordResetUrl (token, returnToUrl) { + let resetUrl = url.resolve(this.host.serverUri, + `/account/password/change?token=${token}`) + + if (returnToUrl) { + resetUrl += `&returnToUrl=${returnToUrl}` + } + + return resetUrl + } + + /** + * Parses and returns an account recovery email stored in a user's root .acl + * + * @param userAccount {UserAccount} + * + * @return {Promise} + */ + loadAccountRecoveryEmail (userAccount) { + return Promise.resolve() + .then(() => { + let rootAclUri = this.rootAclFor(userAccount) + + return this.store.getGraph(rootAclUri) + }) + .then(rootAclGraph => { + let matches = rootAclGraph.match(null, ns.acl('agent')) + + let recoveryMailto = matches.find(agent => { + return agent.object.value.startsWith('mailto:') + }) + + if (recoveryMailto) { + recoveryMailto = recoveryMailto.object.value.replace('mailto:', '') + } + + return recoveryMailto + }) + } + + sendPasswordResetEmail (userAccount, returnToUrl) { + return Promise.resolve() + .then(() => { + if (!this.emailService) { + throw new Error('Email service is not set up') + } + + if (!userAccount.email) { + throw new Error('Account recovery email has not been provided') + } + + return this.generateResetToken(userAccount) + }) + .then(resetToken => { + let resetUrl = this.passwordResetUrl(resetToken, returnToUrl) + + let emailData = { + to: userAccount.email, + webId: userAccount.webId, + resetUrl + } + + return this.emailService.sendWithTemplate('reset-password', emailData) + }) + } + /** * Sends a Welcome email (on new user signup). * diff --git a/lib/models/account-template.js b/lib/models/account-template.js index af9956ca0..cb6ee37aa 100644 --- a/lib/models/account-template.js +++ b/lib/models/account-template.js @@ -12,7 +12,7 @@ const TEMPLATE_FILES = [ 'card' ] /** * Performs account folder initialization from an account template - * (see `./default-account-template/`, for example). + * (see `./default-templates/new-account/`, for example). * * @class AccountTemplate */ @@ -171,8 +171,17 @@ class AccountTemplate { * @return {string} Result, e.g. 'Hello, Alice' */ processTemplate (source) { - let template = Handlebars.compile(source) - return template(this.substitutions) + let template, result + + try { + template = Handlebars.compile(source) + result = template(this.substitutions) + } catch (error) { + console.log('Error processing template: ', error) + return source + } + + return result } /** diff --git a/lib/models/authenticator.js b/lib/models/authenticator.js new file mode 100644 index 000000000..c06e18673 --- /dev/null +++ b/lib/models/authenticator.js @@ -0,0 +1,321 @@ +'use strict' + +const debug = require('./../debug').authentication +const validUrl = require('valid-url') +const webid = require('webid/tls') + +/** + * Abstract Authenticator class, representing a local login strategy. + * To subclass, implement `fromParams()` and `findValidUser()`. + * Used by the `LoginRequest` handler class. + * + * @abstract + * @class Authenticator + */ +class Authenticator { + constructor (options) { + this.accountManager = options.accountManager + } + + /** + * @param req {IncomingRequest} + * @param options {Object} + */ + static fromParams (req, options) { + throw new Error('Must override method') + } + + /** + * @returns {Promise} + */ + findValidUser () { + throw new Error('Must override method') + } +} + +/** + * Authenticates user via Username+Password. + */ +class PasswordAuthenticator extends Authenticator { + /** + * @constructor + * @param options {Object} + * + * @param [options.username] {string} Unique identifier submitted by user + * from the Login form. Can be one of: + * - An account name (e.g. 'alice'), if server is in Multi-User mode + * - A WebID URI (e.g. 'https://alice.example.com/#me') + * + * @param [options.password] {string} Plaintext password as submitted by user + * + * @param [options.userStore] {UserStore} + * + * @param [options.accountManager] {AccountManager} + */ + constructor (options) { + super(options) + + this.userStore = options.userStore + this.username = options.username + this.password = options.password + } + + /** + * Factory method, returns an initialized instance of PasswordAuthenticator + * from an incoming http request. + * + * @param req {IncomingRequest} + * @param [req.body={}] {Object} + * @param [req.body.username] {string} + * @param [req.body.password] {string} + * + * @param options {Object} + * + * @param [options.accountManager] {AccountManager} + * @param [options.userStore] {UserStore} + * + * @return {PasswordAuthenticator} + */ + static fromParams (req, options) { + let body = req.body || {} + + options.username = body.username + options.password = body.password + + return new PasswordAuthenticator(options) + } + + /** + * Ensures required parameters are present, + * and throws an error if not. + * + * @throws {Error} If missing required params + */ + validate () { + let error + + if (!this.username) { + error = new Error('Username required') + error.statusCode = 400 + throw error + } + + if (!this.password) { + error = new Error('Password required') + error.statusCode = 400 + throw error + } + } + + /** + * Loads a user from the user store, and if one is found and the + * password matches, returns a `UserAccount` instance for that user. + * + * @throws {Error} If failures to load user are encountered + * + * @return {Promise} + */ + findValidUser () { + let error + let userOptions + + return Promise.resolve() + .then(() => this.validate()) + .then(() => { + if (validUrl.isUri(this.username)) { + // A WebID URI was entered into the username field + userOptions = { webId: this.username } + } else { + // A regular username + userOptions = { username: this.username } + } + + let user = this.accountManager.userAccountFrom(userOptions) + + debug(`Attempting to login user: ${user.id}`) + + return this.userStore.findUser(user.id) + }) + .then(foundUser => { + if (!foundUser) { + error = new Error('No user found for that username') + error.statusCode = 400 + throw error + } + + return this.userStore.matchPassword(foundUser, this.password) + }) + .then(validUser => { + if (!validUser) { + error = new Error('User found but no password match') + error.statusCode = 400 + throw error + } + + debug('User found, password matches') + + return this.accountManager.userAccountFrom(validUser) + }) + } +} + +/** + * Authenticates a user via a WebID-TLS client side certificate. + */ +class TlsAuthenticator extends Authenticator { + /** + * @constructor + * @param options {Object} + * + * @param [options.accountManager] {AccountManager} + * + * @param [options.connection] {Socket} req.connection + */ + constructor (options) { + super(options) + + this.connection = options.connection + } + + /** + * Factory method, returns an initialized instance of TlsAuthenticator + * from an incoming http request. + * + * @param req {IncomingRequest} + * @param req.connection {Socket} + * + * @param options {Object} + * @param [options.accountManager] {AccountManager} + * + * @return {TlsAuthenticator} + */ + static fromParams (req, options) { + options.connection = req.connection + + return new TlsAuthenticator(options) + } + + /** + * Requests a client certificate from the current TLS connection via + * renegotiation, extracts and verifies the user's WebID URI, + * and makes sure that WebID is hosted on this server. + * + * @throws {Error} If error is encountered extracting the WebID URI from + * certificate, or if the user's account is hosted by a remote system. + * + * @return {Promise} + */ + findValidUser () { + return this.renegotiateTls() + + .then(() => this.getCertificate()) + + .then(cert => this.extractWebId(cert)) + + .then(webId => this.ensureLocalUser(webId)) + } + + /** + * Renegotiates the current TLS connection to ask for a client certificate. + * + * @throws {Error} + * + * @returns {Promise} + */ + renegotiateTls () { + let connection = this.connection + + return new Promise((resolve, reject) => { + connection.renegotiate({ requestCert: true }, (error) => { + if (error) { + debug('Error renegotiating TLS:', error) + + return reject(error) + } + + resolve() + }) + }) + } + + /** + * Requests and returns a client TLS certificate from the current connection. + * + * @throws {Error} If no certificate is presented, or if it is empty. + * + * @return {Promise} + */ + getCertificate () { + let certificate = this.connection.getPeerCertificate() + + if (!certificate || !Object.keys(certificate).length) { + debug('No client certificate detected') + + throw new Error('No client certificate detected. ' + + '(You may need to restart your browser to retry.)') + } + + return certificate + } + + /** + * Extracts (and verifies) the WebID URI from a client certificate. + * + * @param certificate {X509Certificate} + * + * @return {Promise} WebID URI + */ + extractWebId (certificate) { + return new Promise((resolve, reject) => { + this.verifyWebId(certificate, (error, webId) => { + if (error) { + debug('Error processing certificate:', error) + + return reject(error) + } + + resolve(webId) + }) + }) + } + + /** + * Performs WebID-TLS verification (requests the WebID Profile from the + * WebID URI extracted from certificate, and makes sure the public key + * from the profile matches the key from certificate). + * + * @param certificate {X509Certificate} + * @param callback {Function} Gets invoked with signature `callback(error, webId)` + */ + verifyWebId (certificate, callback) { + debug('Verifying WebID URI') + + webid.verify(certificate, callback) + } + + /** + * Ensures that the extracted WebID URI is hosted on this server. If it is, + * returns a UserAccount instance for that WebID, throws an error otherwise. + * + * @param webId {string} + * + * @throws {Error} If the account is not hosted on this server + * + * @return {UserAccount} + */ + ensureLocalUser (webId) { + if (this.accountManager.externalAccount(webId)) { + debug(`WebID URI ${JSON.stringify(webId)} is not a local account`) + + throw new Error('Cannot login: Selected Web ID is not hosted on this server') + } + + return this.accountManager.userAccountFrom({ webId }) + } +} + +module.exports = { + Authenticator, + PasswordAuthenticator, + TlsAuthenticator +} diff --git a/lib/models/email-service.js b/lib/models/email-service.js index 1d43cbfad..8eb9865d3 100644 --- a/lib/models/email-service.js +++ b/lib/models/email-service.js @@ -119,7 +119,7 @@ class EmailService { emailFromTemplate (templateName, data) { let template = this.readTemplate(templateName) - return template.render(data) + return Object.assign({}, template.render(data), data) } /** diff --git a/lib/models/oidc-manager.js b/lib/models/oidc-manager.js new file mode 100644 index 000000000..e355694c7 --- /dev/null +++ b/lib/models/oidc-manager.js @@ -0,0 +1,48 @@ +'use strict' + +const url = require('url') +const path = require('path') +const debug = require('../debug').authentication + +const OidcManager = require('oidc-auth-manager') + +/** + * Returns an instance of the OIDC Authentication Manager, initialized from + * argv / config.json server parameters. + * + * @param argv {Object} Config hashmap + * + * @param argv.host {SolidHost} Initialized SolidHost instance, including + * `serverUri`. + * + * @param [argv.dbPath='./db/oidc'] {string} Path to the auth-related storage + * directory (users, tokens, client registrations, etc, will be stored there). + * + * @param argv.saltRounds {number} Number of bcrypt password salt rounds + * + * @return {OidcManager} Initialized instance, includes a UserStore, + * OIDC Clients store, a Resource Authenticator, and an OIDC Provider. + */ +function fromServerConfig (argv) { + let providerUri = argv.host.serverUri + let authCallbackUri = url.resolve(providerUri, '/api/oidc/rp') + let postLogoutUri = url.resolve(providerUri, '/goodbye') + + let dbPath = path.join(argv.dbPath, 'oidc') + + let options = { + debug, + providerUri, + dbPath, + authCallbackUri, + postLogoutUri, + saltRounds: argv.saltRounds, + host: { debug } + } + + return OidcManager.from(options) +} + +module.exports = { + fromServerConfig +} diff --git a/lib/models/solid-host.js b/lib/models/solid-host.js index 972d75ca1..1a699604e 100644 --- a/lib/models/solid-host.js +++ b/lib/models/solid-host.js @@ -14,11 +14,12 @@ class SolidHost { * @constructor * @param [options={}] * @param [options.port] {number} - * @param [options.serverUri] {string} + * @param [options.serverUri] {string} Fully qualified URI that this Solid + * server is listening on, e.g. `https://databox.me` */ constructor (options = {}) { - this.port = options.port || defaults.DEFAULT_PORT - this.serverUri = options.serverUri || defaults.DEFAULT_URI + this.port = options.port || defaults.port + this.serverUri = options.serverUri || defaults.serverUri this.parsedUri = url.parse(this.serverUri) this.host = this.parsedUri.host @@ -61,6 +62,16 @@ class SolidHost { return this.parsedUri.protocol + '//' + accountName + '.' + this.host } + /** + * Returns the /authorize endpoint URL object (used in login requests, etc). + * + * @return {URL} + */ + get authEndpoint () { + let authUrl = url.resolve(this.serverUri, '/authorize') + return url.parse(authUrl) + } + /** * Returns a cookie domain, based on the current host's serverUri. * diff --git a/lib/token-service.js b/lib/models/token-service.js similarity index 54% rename from lib/token-service.js rename to lib/models/token-service.js index c39b5396a..aefd5dd3d 100644 --- a/lib/token-service.js +++ b/lib/models/token-service.js @@ -1,26 +1,32 @@ 'use strict' const moment = require('moment') -const uid = require('uid-safe').sync -const extend = require('extend') +const ulid = require('ulid') class TokenService { constructor () { this.tokens = {} } - generate (opts = {}) { - const token = uid(20) - this.tokens[token] = { + + generate (data = {}) { + const token = ulid() + + const value = { exp: moment().add(20, 'minutes') } - this.tokens[token] = extend(this.tokens[token], opts) + + this.tokens[token] = Object.assign({}, value, data) return token } + verify (token) { const now = new Date() - if (this.tokens[token] && now < this.tokens[token].exp) { - return this.tokens[token] + + let tokenValue = this.tokens[token] + + if (tokenValue && now < tokenValue.exp) { + return tokenValue } else { return false } diff --git a/lib/models/user-account.js b/lib/models/user-account.js index adf13d82c..5c9b56fa5 100644 --- a/lib/models/user-account.js +++ b/lib/models/user-account.js @@ -41,6 +41,37 @@ class UserAccount { return this.name || this.username || this.email || 'Solid account' } + /** + * Returns the id key for the user account (for use with the user store, for + * example), consisting of the WebID URI minus the protocol and slashes. + * Usage: + * + * ``` + * userAccount.webId = 'https://alice.example.com/profile/card#me' + * userAccount.id // -> 'alice.example.com/profile/card#me' + * ``` + * + * @return {string} + */ + get id () { + if (!this.webId) { return null } + + let parsed = url.parse(this.webId) + let id = parsed.host + parsed.pathname + if (parsed.hash) { + id += parsed.hash + } + return id + } + + get accountUri () { + if (!this.webId) { return null } + + let parsed = url.parse(this.webId) + + return parsed.protocol + '//' + parsed.host + } + /** * Returns the URI of the WebID Profile for this account. * Usage: diff --git a/lib/requests/auth-request.js b/lib/requests/auth-request.js new file mode 100644 index 000000000..767f177b3 --- /dev/null +++ b/lib/requests/auth-request.js @@ -0,0 +1,212 @@ +'use strict' + +const url = require('url') +const debug = require('./../debug').authentication + +/** + * Hidden form fields from the login page that must be passed through to the + * Authentication request. + * + * @type {Array} + */ +const AUTH_QUERY_PARAMS = ['response_type', 'display', 'scope', + 'client_id', 'redirect_uri', 'state', 'nonce'] + +/** + * Base authentication request (used for login and password reset workflows). + */ +class AuthRequest { + /** + * @constructor + * @param [options.response] {ServerResponse} middleware `res` object + * @param [options.session] {Session} req.session + * @param [options.userStore] {UserStore} + * @param [options.accountManager] {AccountManager} + * @param [options.returnToUrl] {string} + * @param [options.authQueryParams] {Object} Key/value hashmap of parsed query + * parameters that will be passed through to the /authorize endpoint. + */ + constructor (options) { + this.response = options.response + this.session = options.session || {} + this.userStore = options.userStore + this.accountManager = options.accountManager + this.returnToUrl = options.returnToUrl + this.authQueryParams = options.authQueryParams || {} + this.localAuth = options.localAuth + } + + /** + * Extracts a given parameter from the request - either from a GET query param, + * a POST body param, or an express registered `/:param`. + * Usage: + * + * ``` + * AuthRequest.parseParameter(req, 'client_id') + * // -> 'client123' + * ``` + * + * @param req {IncomingRequest} + * @param parameter {string} Parameter key + * + * @return {string|null} + */ + static parseParameter (req, parameter) { + let query = req.query || {} + let body = req.body || {} + let params = req.params || {} + + return query[parameter] || body[parameter] || params[parameter] || null + } + + /** + * Extracts the options in common to most auth-related requests. + * + * @param req + * @param res + * + * @return {Object} + */ + static requestOptions (req, res) { + let userStore, accountManager, localAuth + + if (req.app && req.app.locals) { + let locals = req.app.locals + + if (locals.oidc) { + userStore = locals.oidc.users + } + + accountManager = locals.accountManager + + localAuth = locals.localAuth + } + + let authQueryParams = AuthRequest.extractAuthParams(req) + let returnToUrl = AuthRequest.parseParameter(req, 'returnToUrl') + + let options = { + response: res, + session: req.session, + userStore, + accountManager, + returnToUrl, + authQueryParams, + localAuth + } + + return options + } + + /** + * Initializes query params required by Oauth2/OIDC type work flow from the + * request body. + * Only authorized params are loaded, all others are discarded. + * + * @param req {IncomingRequest} + * + * @return {Object} + */ + static extractAuthParams (req) { + let params + if (req.method === 'POST') { + params = req.body + } else { + params = req.query + } + + if (!params) { return {} } + + let extracted = {} + + let paramKeys = AUTH_QUERY_PARAMS + let value + + for (let p of paramKeys) { + value = params[p] + // value = value === 'undefined' ? undefined : value + extracted[p] = value + } + + return extracted + } + + /** + * Calls the appropriate form to display to the user. + * Serves as an error handler for this request workflow. + * + * @param error {Error} + */ + error (error) { + error.statusCode = error.statusCode || 400 + + this.renderForm(error) + } + + /** + * Initializes a session (for subsequent authentication/authorization) with + * a given user's credentials. + * + * @param userAccount {UserAccount} + */ + initUserSession (userAccount) { + let session = this.session + + debug('Initializing user session with webId: ', userAccount.webId) + + session.userId = userAccount.webId + session.identified = true + session.subject = { + _id: userAccount.webId + } + + return userAccount + } + + /** + * Returns this installation's /authorize url. Used for redirecting post-login + * and post-signup. + * + * @return {string} + */ + authorizeUrl () { + let host = this.accountManager.host + let authUrl = host.authEndpoint + + authUrl.query = this.authQueryParams + + return url.format(authUrl) + } + + /** + * Returns this installation's /register url. Used for redirecting post-signup. + * + * @return {string} + */ + registerUrl () { + let host = this.accountManager.host + let signupUrl = url.parse(url.resolve(host.serverUri, '/register')) + + signupUrl.query = this.authQueryParams + + return url.format(signupUrl) + } + + /** + * Returns this installation's /login url. + * + * @return {string} + */ + loginUrl () { + let host = this.accountManager.host + let signupUrl = url.parse(url.resolve(host.serverUri, '/login')) + + signupUrl.query = this.authQueryParams + + return url.format(signupUrl) + } +} + +AuthRequest.AUTH_QUERY_PARAMS = AUTH_QUERY_PARAMS + +module.exports = AuthRequest diff --git a/lib/requests/create-account-request.js b/lib/requests/create-account-request.js index 84ea25fe8..33470a5e6 100644 --- a/lib/requests/create-account-request.js +++ b/lib/requests/create-account-request.js @@ -1,34 +1,37 @@ 'use strict' +const AuthRequest = require('./auth-request') const WebIdTlsCertificate = require('../models/webid-tls-certificate') const debug = require('../debug').accounts /** - * Represents a 'create new user account' http request (a POST to the - * `/accounts/api/new` endpoint). + * Represents a 'create new user account' http request (either a POST to the + * `/accounts/api/new` endpoint, or a GET to `/register`). * * Intended just for browser-based requests; to create new user accounts from * a command line, use the `AccountManager` class directly. * * This is an abstract class, subclasses are created (for example - * `CreateTlsAccountRequest`) depending on which Authentication mode the server + * `CreateOidcAccountRequest`) depending on which Authentication mode the server * is running in. * * @class CreateAccountRequest */ -class CreateAccountRequest { +class CreateAccountRequest extends AuthRequest { /** * @param [options={}] {Object} * @param [options.accountManager] {AccountManager} * @param [options.userAccount] {UserAccount} * @param [options.session] {Session} e.g. req.session * @param [options.response] {HttpResponse} + * @param [options.returnToUrl] {string} If present, redirect the agent to + * this url on successful account creation */ constructor (options) { - this.accountManager = options.accountManager + super(options) + + this.username = options.username this.userAccount = options.userAccount - this.session = options.session - this.response = options.response } /** @@ -37,32 +40,78 @@ class CreateAccountRequest { * * @param req * @param res - * @param accountManager {AccountManager} * - * @throws {TypeError} If required parameters are missing (`userAccountFrom()`), - * or it encounters an unsupported authentication scheme. + * @throws {Error} If required parameters are missing (via + * `userAccountFrom()`), or it encounters an unsupported authentication + * scheme. * * @return {CreateAccountRequest|CreateTlsAccountRequest} */ - static fromParams (req, res, accountManager) { - let userAccount = accountManager.userAccountFrom(req.body) - - let options = { - accountManager, - userAccount, - session: req.session, - response: res + static fromParams (req, res) { + let options = AuthRequest.requestOptions(req, res) + + let locals = req.app.locals + let authMethod = locals.authMethod + let accountManager = locals.accountManager + + let body = req.body || {} + + options.username = body.username + + if (options.username) { + options.userAccount = accountManager.userAccountFrom(body) } - switch (accountManager.authMethod) { + switch (authMethod) { + case 'oidc': + options.password = body.password + return new CreateOidcAccountRequest(options) case 'tls': - options.spkac = req.body.spkac + options.spkac = body.spkac return new CreateTlsAccountRequest(options) default: throw new TypeError('Unsupported authentication scheme') } } + static post (req, res) { + let request = CreateAccountRequest.fromParams(req, res) + + return Promise.resolve() + .then(() => request.validate()) + .then(() => request.createAccount()) + .catch(error => request.error(error)) + } + + static get (req, res) { + let request = CreateAccountRequest.fromParams(req, res) + + return Promise.resolve() + .then(() => request.renderForm()) + .catch(error => request.error(error)) + } + + /** + * Renders the Register form + */ + renderForm (error) { + let authMethod = this.accountManager.authMethod + + let params = Object.assign({}, this.authQueryParams, + { + returnToUrl: this.returnToUrl, + loginUrl: this.loginUrl(), + registerDisabled: authMethod === 'tls' + }) + + if (error) { + params.error = error.message + this.response.status(error.statusCode) + } + + this.response.render('account/register', params) + } + /** * Creates an account for a given user (from a POST to `/api/accounts/new`) * @@ -76,9 +125,9 @@ class CreateAccountRequest { return Promise.resolve(userAccount) .then(this.cancelIfAccountExists.bind(this)) - .then(this.generateCredentials.bind(this)) .then(this.createAccountStorage.bind(this)) - .then(this.initSession.bind(this)) + .then(this.saveCredentialsFor.bind(this)) + .then(this.initUserSession.bind(this)) .then(this.sendResponse.bind(this)) .then(userAccount => { // 'return' not used deliberately, no need to block and wait for email @@ -123,7 +172,7 @@ class CreateAccountRequest { * @param userAccount {UserAccount} Instance of the account to be created * * @throws {Error} If errors were encountering while creating new account - * resources, or saving generated credentials. + * resources. * * @return {Promise} Chainable */ @@ -133,31 +182,76 @@ class CreateAccountRequest { error.message = 'Error creating account storage: ' + error.message throw error }) - .then(() => { - // Once the account folder has been initialized, - // save the public keys or other generated credentials to the profile - return this.saveCredentialsFor(userAccount) - }) .then(() => { debug('Account storage resources created') return userAccount }) } +} +/** + * Models a Create Account request for a server using WebID-OIDC (OpenID Connect) + * as a primary authentication mode. Handles saving user credentials to the + * `UserStore`, etc. + * + * @class CreateOidcAccountRequest + * @extends CreateAccountRequest + */ +class CreateOidcAccountRequest extends CreateAccountRequest { /** - * Initializes the session with the newly created user's credentials + * @constructor * - * @param userAccount {UserAccount} Instance of the account to be created + * @param [options={}] {Object} See `CreateAccountRequest` constructor docstring + * @param [options.password] {string} Password, as entered by the user at signup + */ + constructor (options) { + super(options) + + this.password = options.password + } + + /** + * Validates the Login request (makes sure required parameters are present), + * and throws an error if not. * - * @return {UserAccount} Chainable + * @throws {Error} If missing required params */ - initSession (userAccount) { - let session = this.session + validate () { + let error + + if (!this.username) { + error = new Error('Username required') + error.statusCode = 400 + throw error + } + + if (!this.password) { + error = new Error('Password required') + error.statusCode = 400 + throw error + } + } + + /** + * Generate salted password hash, etc. + * + * @param userAccount {UserAccount} + * + * @return {Promise} + */ + saveCredentialsFor (userAccount) { + return this.userStore.createUser(userAccount, this.password) + .then(() => { + debug('User credentials stored') + return userAccount + }) + } - if (!session) { return userAccount } + sendResponse (userAccount) { + let redirectUrl = this.returnToUrl || + this.accountManager.accountUriFor(userAccount.username) + this.response.redirect(redirectUrl) - session.userId = userAccount.webId - session.identified = true return userAccount } } @@ -176,21 +270,27 @@ class CreateTlsAccountRequest extends CreateAccountRequest { * @param [options={}] {Object} See `CreateAccountRequest` constructor docstring * @param [options.spkac] {string} */ - constructor (options = {}) { + constructor (options) { super(options) + this.spkac = options.spkac this.certificate = null } /** - * Generates required user credentials (WebID-TLS certificate, etc). - * - * @param userAccount {UserAccount} + * Validates the Login request (makes sure required parameters are present), + * and throws an error if not. * - * @return {Promise} Chainable + * @throws {Error} If missing required params */ - generateCredentials (userAccount) { - return this.generateTlsCertificate(userAccount) + validate () { + let error + + if (!this.username) { + error = new Error('Username required') + error.statusCode = 400 + throw error + } } /** @@ -201,8 +301,8 @@ class CreateTlsAccountRequest extends CreateAccountRequest { * @param userAccount {UserAccount} * @param userAccount.webId {string} An agent's WebID URI * - * @throws {Error} HTTP 400 error if errors were encountering during certificate - * generation. + * @throws {Error} HTTP 400 error if errors were encountering during + * certificate generation. * * @return {Promise} Chainable */ @@ -231,22 +331,28 @@ class CreateTlsAccountRequest extends CreateAccountRequest { } /** - * If a WebID-TLS certificate was generated, saves it to the user's profile + * Generates a WebID-TLS certificate and saves it to the user's profile * graph. * * @param userAccount {UserAccount} * - * @return {Promise} + * @return {Promise} Chainable */ saveCredentialsFor (userAccount) { - if (!this.certificate) { - return Promise.resolve(null) - } - - return this.accountManager - .addCertKeyToProfile(this.certificate, userAccount) + return this.generateTlsCertificate(userAccount) + .then(userAccount => { + if (this.certificate) { + return this.accountManager + .addCertKeyToProfile(this.certificate, userAccount) + .then(() => { + debug('Saved generated WebID-TLS certificate to profile') + }) + } else { + debug('No certificate generated, no need to save to profile') + } + }) .then(() => { - debug('Saved generated WebID-TLS certificate to profile') + return userAccount }) } diff --git a/lib/requests/login-request.js b/lib/requests/login-request.js new file mode 100644 index 000000000..7ce699f15 --- /dev/null +++ b/lib/requests/login-request.js @@ -0,0 +1,208 @@ +'use strict' + +const debug = require('./../debug').authentication + +const AuthRequest = require('./auth-request') +const { PasswordAuthenticator, TlsAuthenticator } = require('../models/authenticator') + +const PASSWORD_AUTH = 'password' +const TLS_AUTH = 'tls' + +/** + * Models a local Login request + */ +class LoginRequest extends AuthRequest { + /** + * @constructor + * @param options {Object} + * + * @param [options.response] {ServerResponse} middleware `res` object + * @param [options.session] {Session} req.session + * @param [options.userStore] {UserStore} + * @param [options.accountManager] {AccountManager} + * @param [options.returnToUrl] {string} + * @param [options.authQueryParams] {Object} Key/value hashmap of parsed query + * parameters that will be passed through to the /authorize endpoint. + * @param [options.authenticator] {Authenticator} Auth strategy by which to + * log in + */ + constructor (options) { + super(options) + + this.authenticator = options.authenticator + } + + /** + * Factory method, returns an initialized instance of LoginRequest + * from an incoming http request. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * @param authMethod {string} + * + * @return {LoginRequest} + */ + static fromParams (req, res, authMethod) { + let options = AuthRequest.requestOptions(req, res) + + switch (authMethod) { + case PASSWORD_AUTH: + options.authenticator = PasswordAuthenticator.fromParams(req, options) + break + + case TLS_AUTH: + options.authenticator = TlsAuthenticator.fromParams(req, options) + break + + default: + options.authenticator = null + break + } + + return new LoginRequest(options) + } + + /** + * Handles a Login GET request on behalf of a middleware handler, displays + * the Login page. + * Usage: + * + * ``` + * app.get('/login', LoginRequest.get) + * ``` + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + */ + static get (req, res) { + const request = LoginRequest.fromParams(req, res) + + request.renderForm() + } + + /** + * Handles a Login via Username+Password. + * Errors encountered are displayed on the Login form. + * Usage: + * + * ``` + * app.post('/login/password', LoginRequest.loginPassword) + * ``` + * + * @param req + * @param res + * + * @return {Promise} + */ + static loginPassword (req, res) { + debug('Logging in via username + password') + + let request = LoginRequest.fromParams(req, res, PASSWORD_AUTH) + + return LoginRequest.login(request) + } + + /** + * Handles a Login via WebID-TLS. + * Errors encountered are displayed on the Login form. + * Usage: + * + * ``` + * app.post('/login/tls', LoginRequest.loginTls) + * ``` + * + * @param req + * @param res + * + * @return {Promise} + */ + static loginTls (req, res) { + debug('Logging in via WebID-TLS certificate') + + let request = LoginRequest.fromParams(req, res, TLS_AUTH) + + return LoginRequest.login(request) + } + + /** + * Performs the login operation -- loads and validates the + * appropriate user, inits the session with credentials, and redirects the + * user to continue their auth flow. + * + * @param request {LoginRequest} + * + * @return {Promise} + */ + static login (request) { + return request.authenticator.findValidUser() + + .then(validUser => { + request.initUserSession(validUser) + + request.redirectPostLogin(validUser) + }) + + .catch(error => request.error(error)) + } + + /** + * Returns a URL to redirect the user to after login. + * Either uses the provided `redirect_uri` auth query param, or simply + * returns the user profile URI if none was provided. + * + * @param validUser {UserAccount} + * + * @return {string} + */ + postLoginUrl (validUser) { + let uri + + if (this.authQueryParams['redirect_uri']) { + // Login request is part of an app's auth flow + uri = this.authorizeUrl() + } else if (validUser) { + // Login request is a user going to /login in browser + // uri = this.accountManager.accountUriFor(validUser.username) + uri = validUser.accountUri + } + + return uri + } + + /** + * Redirects the Login request to continue on the OIDC auth workflow. + */ + redirectPostLogin (validUser) { + let uri = this.postLoginUrl(validUser) + + debug('Login successful, redirecting to ', uri) + + this.response.redirect(uri) + } + + /** + * Renders the login form + */ + renderForm (error) { + let params = Object.assign({}, this.authQueryParams, + { + registerUrl: this.registerUrl(), + returnToUrl: this.returnToUrl, + enablePassword: this.localAuth.password, + enableTls: this.localAuth.tls + }) + + if (error) { + params.error = error.message + this.response.status(error.statusCode) + } + + this.response.render('auth/login', params) + } +} + +module.exports = { + LoginRequest, + PASSWORD_AUTH, + TLS_AUTH +} diff --git a/lib/requests/password-change-request.js b/lib/requests/password-change-request.js new file mode 100644 index 000000000..5ac1a76a9 --- /dev/null +++ b/lib/requests/password-change-request.js @@ -0,0 +1,201 @@ +'use strict' + +const AuthRequest = require('./auth-request') +const debug = require('./../debug').accounts + +class PasswordChangeRequest extends AuthRequest { + /** + * @constructor + * @param options {Object} + * @param options.accountManager {AccountManager} + * @param options.userStore {UserStore} + * @param options.response {ServerResponse} express response object + * @param [options.token] {string} One-time reset password token (from email) + * @param [options.returnToUrl] {string} + * @param [options.newPassword] {string} New password to save + */ + constructor (options) { + super(options) + + this.token = options.token + this.returnToUrl = options.returnToUrl + + this.validToken = false + + this.newPassword = options.newPassword + } + + /** + * Factory method, returns an initialized instance of PasswordChangeRequest + * from an incoming http request. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * + * @return {PasswordChangeRequest} + */ + static fromParams (req, res) { + let locals = req.app.locals + let accountManager = locals.accountManager + let userStore = locals.oidc.users + + let returnToUrl = this.parseParameter(req, 'returnToUrl') + let token = this.parseParameter(req, 'token') + let oldPassword = this.parseParameter(req, 'password') + let newPassword = this.parseParameter(req, 'newPassword') + + let options = { + accountManager, + userStore, + returnToUrl, + token, + oldPassword, + newPassword, + response: res + } + + return new PasswordChangeRequest(options) + } + + /** + * Handles a Change Password GET request on behalf of a middleware handler. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * + * @return {Promise} + */ + static get (req, res) { + const request = PasswordChangeRequest.fromParams(req, res) + + return Promise.resolve() + .then(() => request.validateToken()) + .then(() => request.renderForm()) + .catch(error => request.error(error)) + } + + /** + * Handles a Change Password POST request on behalf of a middleware handler. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * + * @return {Promise} + */ + static post (req, res) { + const request = PasswordChangeRequest.fromParams(req, res) + + return PasswordChangeRequest.handlePost(request) + } + + /** + * Performs the 'Change Password' operation, after the user submits the + * password change form. Validates the parameters (the one-time token, + * the new password), changes the password, and renders the success view. + * + * @param request {PasswordChangeRequest} + * + * @return {Promise} + */ + static handlePost (request) { + return Promise.resolve() + .then(() => request.validatePost()) + .then(() => request.validateToken()) + .then(tokenContents => request.changePassword(tokenContents)) + .then(() => request.renderSuccess()) + .catch(error => request.error(error)) + } + + /** + * Validates the 'Change Password' parameters, and throws an error if any + * validation fails. + * + * @throws {Error} + */ + validatePost () { + if (!this.newPassword) { + throw new Error('Please enter a new password') + } + } + + /** + * Validates the one-time Password Reset token that was emailed to the user. + * If the token service has a valid token saved for the given key, it returns + * the token object value (which contains the user's WebID URI, etc). + * If no token is saved, returns `false`. + * + * @return {Promise} + */ + validateToken () { + return Promise.resolve() + .then(() => { + if (!this.token) { return false } + + return this.accountManager.validateResetToken(this.token) + }) + .then(validToken => { + if (validToken) { + this.validToken = true + } + + return validToken + }) + .catch(error => { + this.token = null + throw error + }) + } + + /** + * Changes the password that's saved in the user store. + * If the user has no user store entry, it creates one. + * + * @param tokenContents {Object} + * @param tokenContents.webId {string} + * + * @return {Promise} + */ + changePassword (tokenContents) { + let user = this.accountManager.userAccountFrom(tokenContents) + + debug('Changing password for user:', user.webId) + + return this.userStore.findUser(user.id) + .then(userStoreEntry => { + if (userStoreEntry) { + return this.userStore.updatePassword(user, this.newPassword) + } else { + return this.userStore.createUser(user, this.newPassword) + } + }) + } + + /** + * Renders the 'change password' form. + * + * @param [error] {Error} Optional error to display + */ + renderForm (error) { + let params = { + validToken: this.validToken, + returnToUrl: this.returnToUrl, + token: this.token + } + + if (error) { + params.error = error.message + this.response.status(error.statusCode) + } + + this.response.render('auth/change-password', params) + } + + /** + * Displays the 'password has been changed' success view. + */ + renderSuccess () { + this.response.render('auth/password-changed', { returnToUrl: this.returnToUrl }) + } +} + +module.exports = PasswordChangeRequest diff --git a/lib/requests/password-reset-email-request.js b/lib/requests/password-reset-email-request.js new file mode 100644 index 000000000..4bff4594f --- /dev/null +++ b/lib/requests/password-reset-email-request.js @@ -0,0 +1,199 @@ +'use strict' + +const AuthRequest = require('./auth-request') +const debug = require('./../debug').accounts + +class PasswordResetEmailRequest extends AuthRequest { + /** + * @constructor + * @param options {Object} + * @param options.accountManager {AccountManager} + * @param options.response {ServerResponse} express response object + * @param [options.returnToUrl] {string} + * @param [options.username] {string} Username / account name (e.g. 'alice') + */ + constructor (options) { + super(options) + + this.returnToUrl = options.returnToUrl + this.username = options.username + } + + /** + * Factory method, returns an initialized instance of PasswordResetEmailRequest + * from an incoming http request. + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + * + * @return {PasswordResetEmailRequest} + */ + static fromParams (req, res) { + let locals = req.app.locals + let accountManager = locals.accountManager + + let returnToUrl = this.parseParameter(req, 'returnToUrl') + let username = this.parseParameter(req, 'username') + + let options = { + accountManager, + returnToUrl, + username, + response: res + } + + return new PasswordResetEmailRequest(options) + } + + /** + * Handles a Reset Password GET request on behalf of a middleware handler. + * Usage: + * + * ``` + * app.get('/password/reset', PasswordResetEmailRequest.get) + * ``` + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + */ + static get (req, res) { + const request = PasswordResetEmailRequest.fromParams(req, res) + + request.renderForm() + } + + /** + * Handles a Reset Password POST request on behalf of a middleware handler. + * Usage: + * + * ``` + * app.get('/password/reset', PasswordResetEmailRequest.get) + * ``` + * + * @param req {IncomingRequest} + * @param res {ServerResponse} + */ + static post (req, res) { + const request = PasswordResetEmailRequest.fromParams(req, res) + + debug(`User '${request.username}' requested to be sent a password reset email`) + + return PasswordResetEmailRequest.handlePost(request) + } + + /** + * Performs a 'send me a password reset email' request operation, after the + * user has entered an email into the reset form. + * + * @param request {IncomingRequest} + * + * @return {Promise} + */ + static handlePost (request) { + return Promise.resolve() + .then(() => request.validate()) + .then(() => request.loadUser()) + .then(userAccount => request.sendResetLink(userAccount)) + .then(() => request.renderSuccess()) + .catch(error => request.error(error)) + } + + /** + * Validates the request parameters, and throws an error if any + * validation fails. + * + * @throws {Error} + */ + validate () { + if (this.accountManager.multiUser && !this.username) { + throw new Error('Username required') + } + } + + /** + * Returns a user account instance for the submitted username. + * + * @throws {Error} Rejects if user account does not exist for the username + * + * @returns {Promise} + */ + loadUser () { + let username = this.username + + return this.accountManager.accountExists(username) + .then(exists => { + if (!exists) { + throw new Error('Account not found for that username') + } + + let userData = { username } + + return this.accountManager.userAccountFrom(userData) + }) + } + + /** + * Loads the account recovery email for a given user and sends out a + * password request email. + * + * @param userAccount {UserAccount} + * + * @return {Promise} + */ + sendResetLink (userAccount) { + let accountManager = this.accountManager + + return accountManager.loadAccountRecoveryEmail(userAccount) + .then(recoveryEmail => { + userAccount.email = recoveryEmail + + debug('Sending recovery email to:', recoveryEmail) + + return accountManager + .sendPasswordResetEmail(userAccount, this.returnToUrl) + }) + } + + /** + * Renders the 'send password reset link' form along with the provided error. + * Serves as an error handler for this request workflow. + * + * @param error {Error} + */ + error (error) { + let res = this.response + + debug(error) + + let params = { + error: error.message, + returnToUrl: this.returnToUrl, + multiUser: this.accountManager.multiUser + } + + res.status(error.statusCode || 400) + + res.render('auth/reset-password', params) + } + + /** + * Renders the 'send password reset link' form + */ + renderForm () { + let params = { + returnToUrl: this.returnToUrl, + multiUser: this.accountManager.multiUser + } + + this.response.render('auth/reset-password', params) + } + + /** + * Displays the 'your reset link has been sent' success message view + */ + renderSuccess () { + this.response.render('auth/reset-link-sent') + } +} + +module.exports = PasswordResetEmailRequest diff --git a/lib/server-config.js b/lib/server-config.js new file mode 100644 index 000000000..80e0b9387 --- /dev/null +++ b/lib/server-config.js @@ -0,0 +1,137 @@ +'use strict' +/** + * Server config initialization utilities + */ + +const fs = require('fs-extra') +const path = require('path') + +/** + * Ensures that a directory has been copied / initialized. Used to ensure that + * account templates, email templates and default apps have been copied from + * their defaults to the customizable config directory, at server startup. + * + * @param fromDir {string} Path to copy from (defaults) + * + * @param toDir {string} Path to copy to (customizable config) + * + * @return {string} Returns the absolute path for `toDir` + */ +function ensureDirCopyExists (fromDir, toDir) { + fromDir = path.resolve(fromDir) + toDir = path.resolve(toDir) + + if (!fs.existsSync(toDir)) { + fs.copySync(fromDir, toDir) + } + + return toDir +} + +/** + * Creates (copies from the server templates dir) a Welcome index page for the + * server root web directory, if one does not already exist. This page + * typically has links to account signup and login, and can be overridden by + * the server operator. + * + * @param argv {Function} Express.js app object + */ +function ensureWelcomePage (argv) { + let multiUser = argv.idp + let rootDir = path.resolve(argv.root) + let templates = argv.templates + let serverRootDir + + if (multiUser) { + serverRootDir = path.join(rootDir, argv.host.hostname) + } else { + serverRootDir = rootDir + } + + let defaultIndexPage = path.join(templates.server, 'index.html') + let existingIndexPage = path.join(serverRootDir, 'index.html') + let defaultIndexPageAcl = path.join(templates.server, 'index.html.acl') + let existingIndexPageAcl = path.join(serverRootDir, 'index.html.acl') + + if (!fs.existsSync(existingIndexPage)) { + fs.mkdirp(serverRootDir) + fs.copySync(defaultIndexPage, existingIndexPage) + fs.copySync(defaultIndexPageAcl, existingIndexPageAcl) + } +} + +/** + * Ensures that the server config directory (something like '/etc/solid-server' + * or './config', taken from the `configPath` config.json file) exists, and + * creates it if not. + * + * @param argv + * + * @return {string} Path to the server config dir + */ +function initConfigDir (argv) { + let configPath = path.resolve(argv.configPath) + fs.mkdirp(configPath) + + return configPath +} + +/** + * Ensures that the customizable 'views' folder exists for this installation + * (copies it from default views if not). + * + * @param configPath {string} Location of configuration directory (from the + * local config.json file or passed in as cli parameter) + * + * @return {string} Path to the views dir + */ +function initDefaultViews (configPath) { + let defaultViewsPath = path.resolve('./default-views') + let viewsPath = path.join(configPath, 'views') + + ensureDirCopyExists(defaultViewsPath, viewsPath) + + return viewsPath +} + +/** + * Makes sure that the various template directories (email templates, new + * account templates, etc) have been copied from the default directories to + * this server's own config directory. + * + * @param configPath {string} Location of configuration directory (from the + * local config.json file or passed in as cli parameter) + * + * @return {Object} Returns a hashmap of template directories by type + * (new account, email, server) + */ +function initTemplateDirs (configPath) { + let accountTemplatePath = ensureDirCopyExists( + './default-templates/new-account', + path.join(configPath, 'templates', 'new-account') + ) + + let emailTemplatesPath = ensureDirCopyExists( + './default-templates/emails', + path.join(configPath, 'templates', 'emails') + ) + + let serverTemplatePath = ensureDirCopyExists( + './default-templates/server', + path.join(configPath, 'templates', 'server') + ) + + return { + account: accountTemplatePath, + email: emailTemplatesPath, + server: serverTemplatePath + } +} + +module.exports = { + ensureDirCopyExists, + ensureWelcomePage, + initConfigDir, + initDefaultViews, + initTemplateDirs +} diff --git a/lib/utils.js b/lib/utils.js index f3228a525..d9f559141 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -13,15 +13,52 @@ module.exports.stringToStream = stringToStream module.exports.reqToPath = reqToPath module.exports.debrack = debrack module.exports.stripLineEndings = stripLineEndings +module.exports.fullUrlForReq = fullUrlForReq var fs = require('fs') var path = require('path') var S = require('string') var $rdf = require('rdflib') var from = require('from2') +var url = require('url') +/** + * Returns a fully qualified URL from an Express.js Request object. + * (It's insane that Express does not provide this natively.) + * + * Usage: + * + * ``` + * console.log(util.fullUrlForReq(req)) + * // -> https://example.com/path/to/resource?q1=v1 + * ``` + * + * @param req {IncomingRequest} + * + * @return {string} + */ +function fullUrlForReq (req) { + let fullUrl = url.format({ + protocol: req.protocol, + host: req.get('host'), + pathname: url.resolve(req.baseUrl, req.path), + query: req.query + }) + + return fullUrl +} + +/** + * Removes the `<` and `>` brackets around a string and returns it. + * Used by the `allow` handler in `verifyDelegator()` logic. + * @method debrack + * + * @param s {string} + * + * @return {string} + */ function debrack (s) { - if (s.length < 2) { + if (!s || s.length < 2) { return s } if (s[0] !== '<') { diff --git a/package.json b/package.json index 914cc3996..56db4dc24 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,12 @@ "cors": "^2.7.1", "debug": "^2.2.0", "express": "^4.13.3", + "express-handlebars": "^3.0.0", "express-session": "^1.11.3", "extend": "^3.0.0", "from2": "^2.1.0", - "fs-extra": "^0.30.0", - "glob": "^7.0.0", + "fs-extra": "^2.1.0", + "glob": "^7.1.1", "handlebars": "^4.0.6", "inquirer": "^1.0.2", "ip-range-check": "0.0.1", @@ -51,11 +52,14 @@ "jsonld": "^0.4.5", "li": "^1.0.1", "mime-types": "^2.1.11", - "moment": "^2.13.0", + "moment": "^2.18.1", "negotiator": "^0.6.0", + "node-fetch": "^1.6.3", "node-forge": "^0.6.38", "nodemailer": "^3.1.4", "nomnom": "^1.8.1", + "oidc-auth-manager": "^0.7.2", + "oidc-op-express": "^0.0.3", "rdflib": "^0.12.3", "recursive-readdir": "^2.1.0", "request": "^2.72.0", @@ -65,32 +69,35 @@ "solid-permissions": "^0.5.1", "solid-ws": "^0.2.3", "string": "^3.3.0", - "uid-safe": "^2.1.1", + "ulid": "^0.1.0", "uuid": "^3.0.0", "valid-url": "^1.0.9", "vhost": "^3.0.2", "webid": "^0.3.7" }, "devDependencies": { - "chai": "^3.0.0", + "chai": "^3.5.0", + "chai-as-promised": "^6.0.0", + "dirty-chai": "^1.2.2", "hippie": "^0.5.0", "mocha": "^3.2.0", "nock": "^9.0.2", "node-mocks-http": "^1.5.6", "nyc": "^10.1.2", "proxyquire": "^1.7.10", - "run-waterfall": "^1.1.3", - "sinon": "^1.17.7", + "sinon": "^2.1.0", "sinon-chai": "^2.8.0", "standard": "^8.6.0", - "supertest": "^1.2.0" + "supertest": "^3.0.0" }, "main": "index.js", "scripts": { "solid": "node ./bin/solid.js", "standard": "standard", - "mocha": "nyc mocha ./test/*.js", + "mocha": "nyc mocha ./test/**/*.js", "test": "npm run standard && npm run mocha", + "test-integration": "mocha ./test/integration/*.js", + "test-unit": "mocha ./test/unit/*.js", "test-debug": "DEBUG='solid:*' ./node_modules/mocha/bin/mocha ./test/*.js", "test-acl": "./node_modules/mocha/bin/mocha ./test/acl.js", "test-params": "./node_modules/mocha/bin/mocha ./test/params.js", diff --git a/test/api-accounts.js b/test/api-accounts.js deleted file mode 100644 index 468c185cc..000000000 --- a/test/api-accounts.js +++ /dev/null @@ -1,131 +0,0 @@ -const Solid = require('../') -const parallel = require('run-parallel') -const waterfall = require('run-waterfall') -const path = require('path') -const supertest = require('supertest') -const expect = require('chai').expect -const nock = require('nock') -// In this test we always assume that we are Alice - -describe('Accounts API', () => { - let aliceServer - let bobServer - let alice - let bob - - const alicePod = Solid.createServer({ - root: path.join(__dirname, '/resources/accounts-scenario/alice'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), - auth: 'oidc', - dataBrowser: false, - fileBrowser: false, - webid: true - }) - const bobPod = Solid.createServer({ - root: path.join(__dirname, '/resources/accounts-scenario/bob'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), - auth: 'oidc', - dataBrowser: false, - fileBrowser: false, - webid: true - }) - - function getBobFoo (alice, bob, done) { - bob.get('/foo') - .expect(401) - .end((err, res) => { - if (err) return done(err) - expect(res).to.match(/META http-equiv="refresh"/) - done() - }) - } - - function postBobDiscoverSignIn (alice, bob, done) { - done() - } - - function entersPasswordAndConsent (alice, bob, done) { - done() - } - - before(function (done) { - parallel([ - (cb) => { - aliceServer = alicePod.listen(5000, cb) - alice = supertest('https://localhost:5000') - }, - (cb) => { - bobServer = bobPod.listen(5001, cb) - bob = supertest('https://localhost:5001') - } - ], done) - }) - - after(function () { - if (aliceServer) aliceServer.close() - if (bobServer) bobServer.close() - }) - - describe('endpoints', () => { - describe('/api/accounts/signin', () => { - it('should complain if a URL is missing', (done) => { - alice.post('/api/accounts/signin') - .expect(400) - .end(done) - }) - it('should complain if a URL is invalid', (done) => { - alice.post('/api/accounts/signin') - .send('webid=HELLO') - .expect(400) - .end(done) - }) - it("should return a 400 if endpoint doesn't have Link Headers", (done) => { - nock('https://amazingwebsite.tld').intercept('/', 'OPTIONS').reply(200) - alice.post('/api/accounts/signin') - .send('webid=https://amazingwebsite.tld/') - .expect(400) - .end(done) - }) - it("should return a 400 if endpoint doesn't have oidc in the headers", (done) => { - nock('https://amazingwebsite.tld') - .intercept('/', 'OPTIONS') - .reply(200, '', { - 'Link': function (req, res, body) { - return '; rel="oidc.issuer"' - } - }) - alice.post('/api/accounts/signin') - .send('webid=https://amazingwebsite.tld/') - .expect(302) - .end((err, res) => { - expect(res.header.location).to.eql('https://oidc.amazingwebsite.tld') - done(err) - }) - }) - }) - }) - - describe('Auth workflow', () => { - it.skip('step1: User tries to get /foo and gets 401 and meta redirect', (done) => { - getBobFoo(alice, bob, done) - }) - - it.skip('step2: User enters webId to signin', (done) => { - postBobDiscoverSignIn(alice, bob, done) - }) - - it.skip('step3: User enters password', (done) => { - entersPasswordAndConsent(alice, bob, done) - }) - - it.skip('entire flow', (done) => { - waterfall([ - (cb) => getBobFoo(alice, bob, cb), - (cb) => postBobDiscoverSignIn(alice, bob, cb), - (cb) => entersPasswordAndConsent(alice, bob, cb) - ], done) - }) - }) -}) diff --git a/test/api-messages.js b/test/api-messages.js deleted file mode 100644 index 39cb1a586..000000000 --- a/test/api-messages.js +++ /dev/null @@ -1,121 +0,0 @@ -const Solid = require('../') -const parallel = require('run-parallel') -const path = require('path') -const hippie = require('hippie') -const fs = require('fs') -// In this test we always assume that we are Alice - -describe.skip('Messages API', () => { - let aliceServer - - const bobCert = { - cert: fs.readFileSync(path.join(__dirname, '/keys/user2-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user2-key.pem')) - } - const aliceCert = { - cert: fs.readFileSync(path.join(__dirname, '/keys/user1-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user1-key.pem')) - } - - const alicePod = Solid.createServer({ - root: path.join(__dirname, '/resources/messaging-scenario'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), - auth: 'tls', - dataBrowser: false, - fileBrowser: false, - webid: true, - idp: true - }) - - before(function (done) { - parallel([ - (cb) => { - aliceServer = alicePod.listen(5000, cb) - } - ], done) - }) - - after(function () { - if (aliceServer) aliceServer.close() - }) - - describe('endpoints', () => { - describe('/api/messages', () => { - it('should send 401 if user is not logged in', (done) => { - hippie() - .post('https://localhost:5000/api/messages') - .expectStatus(401) - .end(done) - }) - it('should send 406 if message is missing', (done) => { - hippie() - // .json() - .use(function (options, next) { - options.agentOptions = bobCert - options.strictSSL = false - next(options) - }) - .post('https://localhost:5000/api/messages') - .expectStatus(406) - .end(done) - }) - it('should send 403 user is not of this IDP', (done) => { - hippie() - // .json() - .use(function (options, next) { - options.agentOptions = bobCert - options.strictSSL = false - next(options) - }) - .form() - .send({message: 'thisisamessage', to: 'mailto:mail@email.com'}) - .post('https://localhost:5000/api/messages') - .expectStatus(403) - .end(done) - }) - it('should send 406 if not destination `to` is specified', (done) => { - hippie() - // .json() - .use(function (options, next) { - options.agentOptions = aliceCert - options.strictSSL = false - next(options) - }) - .form() - .send({message: 'thisisamessage'}) - .post('https://localhost:5000/api/messages') - .expectStatus(406) - .end(done) - }) - it('should send 406 if not destination `to` is missing the protocol', (done) => { - hippie() - // .json() - .use(function (options, next) { - options.agentOptions = aliceCert - options.strictSSL = false - next(options) - }) - .form() - .send({message: 'thisisamessage', to: 'mail@email.com'}) - .post('https://localhost:5000/api/messages') - .expectStatus(406) - .end(done) - }) - it('should send 406 if messaging protocol is not supported', (done) => { - hippie() - // .json() - .use(function (options, next) { - options.agentOptions = aliceCert - options.strictSSL = false - next(options) - }) - .form() - .send({message: 'thisisamessage', to: 'email2:mail@email.com'}) - .post('https://localhost:5000/api/messages') - .expectStatus(406) - .end(done) - }) - }) - }) -}) diff --git a/test/capability-discovery.js b/test/capability-discovery.js deleted file mode 100644 index 63f5ce4ec..000000000 --- a/test/capability-discovery.js +++ /dev/null @@ -1,84 +0,0 @@ -const Solid = require('../') -const parallel = require('run-parallel') -const path = require('path') -const supertest = require('supertest') -const expect = require('chai').expect -// In this test we always assume that we are Alice - -describe('API', () => { - let aliceServer - let alice - - const alicePod = Solid.createServer({ - root: path.join(__dirname, '/resources/accounts-scenario/alice'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), - auth: 'oidc', - dataBrowser: false, - fileBrowser: false, - webid: true - }) - - before(function (done) { - parallel([ - (cb) => { - aliceServer = alicePod.listen(5000, cb) - alice = supertest('https://localhost:5000') - } - ], done) - }) - - after(function () { - if (aliceServer) aliceServer.close() - }) - - describe('Capability Discovery', function () { - describe('GET Service Capability document', function () { - it('should exist', function (done) { - alice.get('/.well-known/solid') - .expect(200, done) - }) - it('should be a json file by default', function (done) { - alice.get('/.well-known/solid') - .expect('content-type', /application\/json/) - .expect(200, done) - }) - it('includes a root element', function (done) { - alice.get('/.well-known/solid') - .end(function (err, req) { - expect(req.body.root).to.exist - return done(err) - }) - }) - it('includes an apps config section', function (done) { - const config = { - apps: { - 'signin': '/signin/', - 'signup': '/signup/' - } - } - const solid = Solid(config) - let server = supertest(solid) - server.get('/.well-known/solid') - .end(function (err, req) { - expect(req.body.apps).to.exist - return done(err) - }) - }) - }) - - describe('OPTIONS API', function () { - it('should set the service Link header', function (done) { - alice.options('/') - .expect('Link', /<.*\.well-known\/solid>; rel="service"/) - .expect(204, done) - }) - it('should still have previous link headers', function (done) { - alice.options('/') - .expect('Link', /; rel="type"/) - .expect('Link', /; rel="type"/) - .expect(204, done) - }) - }) - }) -}) diff --git a/test/create-account-request.js b/test/create-account-request.js deleted file mode 100644 index 325ec9537..000000000 --- a/test/create-account-request.js +++ /dev/null @@ -1,209 +0,0 @@ -'use strict' - -const chai = require('chai') -const expect = chai.expect -const sinon = require('sinon') -const sinonChai = require('sinon-chai') -chai.use(sinonChai) -chai.should() -const HttpMocks = require('node-mocks-http') - -const LDP = require('../lib/ldp') -const AccountManager = require('../lib/models/account-manager') -const SolidHost = require('../lib/models/solid-host') -const { CreateAccountRequest } = require('../lib/requests/create-account-request') - -var host, store, accountManager -var aliceData, userAccount -var req, session, res - -beforeEach(() => { - host = SolidHost.from({ serverUri: 'https://example.com' }) - store = new LDP() - accountManager = AccountManager.from({ - host, - store, - authMethod: 'tls', - multiUser: true - }) - - aliceData = { - username: 'alice', - spkac: '123' - } - userAccount = accountManager.userAccountFrom(aliceData) - - session = {} - req = { - body: aliceData, - session - } - res = HttpMocks.createResponse() -}) - -describe('CreateAccountRequest', () => { - describe('constructor()', () => { - it('should create an instance with the given config', () => { - let options = { accountManager, userAccount, session, response: res } - let request = new CreateAccountRequest(options) - - expect(request.accountManager).to.equal(accountManager) - expect(request.userAccount).to.equal(userAccount) - expect(request.session).to.equal(session) - expect(request.response).to.equal(res) - }) - }) - - describe('fromParams()', () => { - it('should create subclass depending on authMethod', () => { - let accountManager = AccountManager.from({ host, authMethod: 'tls' }) - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - expect(request).to.respondTo('generateTlsCertificate') - }) - }) - - describe('createAccount()', () => { - it('should return a 400 error if account already exists', done => { - let accountManager = AccountManager.from({ host }) - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - accountManager.accountExists = sinon.stub().returns(Promise.resolve(true)) - - request.createAccount() - .catch(err => { - expect(err.status).to.equal(400) - done() - }) - }) - - it('should return a UserAccount instance', () => { - let multiUser = true - let accountManager = AccountManager.from({ host, store, multiUser }) - accountManager.accountExists = sinon.stub().returns(Promise.resolve(false)) - accountManager.createAccountFor = sinon.stub().returns(Promise.resolve()) - - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.sendResponse = sinon.stub() - request.generateCredentials = (userAccount) => { - return Promise.resolve(userAccount) - } - - return request.createAccount() - .then(newUser => { - expect(newUser.username).to.equal('alice') - expect(newUser.webId).to.equal('https://alice.example.com/profile/card#me') - }) - }) - - it('should call generateCredentials()', () => { - let accountManager = AccountManager.from({ host, store }) - accountManager.accountExists = sinon.stub().returns(Promise.resolve(false)) - accountManager.createAccountFor = sinon.stub().returns(Promise.resolve()) - - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.generateCredentials = (userAccount) => { - return Promise.resolve(userAccount) - } - request.sendResponse = sinon.stub() - let generateCredentials = sinon.spy(request, 'generateCredentials') - - return request.createAccount() - .then(() => { - expect(generateCredentials).to.have.been.called - }) - }) - - it('should call createAccountStorage()', () => { - let accountManager = AccountManager.from({ host, store }) - accountManager.accountExists = sinon.stub().returns(Promise.resolve(false)) - accountManager.createAccountFor = sinon.stub().returns(Promise.resolve()) - - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.generateCredentials = (userAccount) => { - return Promise.resolve(userAccount) - } - request.sendResponse = sinon.stub() - let createAccountStorage = sinon.spy(request, 'createAccountStorage') - - return request.createAccount() - .then(() => { - expect(createAccountStorage).to.have.been.called - }) - }) - - it('should call initSession()', () => { - let accountManager = AccountManager.from({ host, store }) - accountManager.accountExists = sinon.stub().returns(Promise.resolve(false)) - accountManager.createAccountFor = sinon.stub().returns(Promise.resolve()) - - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.generateCredentials = (userAccount) => { - return Promise.resolve(userAccount) - } - request.sendResponse = sinon.stub() - let initSession = sinon.spy(request, 'initSession') - - return request.createAccount() - .then(() => { - expect(initSession).to.have.been.called - }) - }) - - it('should call sendResponse()', () => { - let accountManager = AccountManager.from({ host, store }) - accountManager.accountExists = sinon.stub().returns(Promise.resolve(false)) - accountManager.createAccountFor = sinon.stub().returns(Promise.resolve()) - - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.generateCredentials = (userAccount) => { - return Promise.resolve(userAccount) - } - request.sendResponse = sinon.stub() - - return request.createAccount() - .then(() => { - expect(request.sendResponse).to.have.been.called - }) - }) - }) -}) - -describe('CreateTlsAccountRequest', () => { - let authMethod = 'tls' - - describe('fromParams()', () => { - it('should create an instance with the given config', () => { - let accountManager = AccountManager.from({ host, store, authMethod }) - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - expect(request.accountManager).to.equal(accountManager) - expect(request.userAccount.username).to.equal('alice') - expect(request.session).to.equal(session) - expect(request.response).to.equal(res) - expect(request.spkac).to.equal(aliceData.spkac) - }) - }) - - describe('generateCredentials()', () => { - it('should call generateTlsCertificate()', () => { - let accountManager = AccountManager.from({ host, store, authMethod }) - let request = CreateAccountRequest.fromParams(req, res, accountManager) - - request.generateTlsCertificate = (userAccount) => { - return Promise.resolve(userAccount) - } - let generateTlsCertificate = sinon.spy(request, 'generateTlsCertificate') - - return request.generateCredentials(userAccount) - .then(() => { - expect(generateTlsCertificate).to.have.been.calledWith(userAccount) - }) - }) - }) -}) diff --git a/test/account-creation.js b/test/integration/account-creation-oidc.js similarity index 71% rename from test/account-creation.js rename to test/integration/account-creation-oidc.js index 5e862480d..f8610d9f5 100644 --- a/test/account-creation.js +++ b/test/integration/account-creation-oidc.js @@ -1,25 +1,31 @@ -var supertest = require('supertest') +const supertest = require('supertest') // Helper functions for the FS -var rm = require('./test-utils').rm -var $rdf = require('rdflib') -var read = require('./test-utils').read -var ldnode = require('../index') -var path = require('path') - -describe('AccountManager (account creation tests)', function () { - this.timeout(10000) +const $rdf = require('rdflib') + +const { rm, read } = require('../test-utils') +const ldnode = require('../../index') +const path = require('path') +const fs = require('fs-extra') + +describe('AccountManager (OIDC account creation tests)', function () { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' - var address = 'https://localhost:3457' + var serverUri = 'https://localhost:3457' var host = 'localhost:3457' var ldpHttpsServer + let rootPath = path.join(__dirname, '../resources/accounts/') + let dbPath = path.join(__dirname, '../resources/accounts/db') + var ldp = ldnode.createServer({ - root: path.join(__dirname, '/resources/accounts/'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), + root: rootPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + auth: 'oidc', webid: true, idp: true, - strictOrigin: true + strictOrigin: true, + dbPath, + serverUri }) before(function (done) { @@ -28,9 +34,12 @@ describe('AccountManager (account creation tests)', function () { after(function () { if (ldpHttpsServer) ldpHttpsServer.close() + fs.removeSync(path.join(dbPath, 'oidc/users/users')) + fs.removeSync(path.join(rootPath, 'localhost/index.html')) + fs.removeSync(path.join(rootPath, 'localhost/index.html.acl')) }) - var server = supertest(address) + var server = supertest(serverUri) it('should expect a 404 on GET /accounts', function (done) { server.get('/api/accounts') @@ -57,38 +66,6 @@ describe('AccountManager (account creation tests)', function () { }) }) - describe('generating a certificate', () => { - beforeEach(function () { - rm('accounts/nicola.localhost') - }) - after(function () { - rm('accounts/nicola.localhost') - }) - - it('should generate a certificate if spkac is valid', (done) => { - var spkac = read('example_spkac.cnf') - var subdomain = supertest.agent('https://nicola.' + host) - subdomain.post('/api/accounts/new') - .send('username=nicola&spkac=' + spkac) - .expect('Content-Type', /application\/x-x509-user-cert/) - .expect(200, done) - }) - - it('should not generate a certificate if spkac is not valid', (done) => { - var subdomain = supertest('https://nicola.' + host) - subdomain.post('/api/accounts/new') - .send('username=nicola') - .expect(200) - .end((err) => { - if (err) return done(err) - - subdomain.post('/api/accounts/cert') - .send('username=nicola&spkac=') - .expect(400, done) - }) - }) - }) - describe('creating an account with POST', function () { beforeEach(function () { rm('accounts/nicola.localhost') @@ -100,24 +77,29 @@ describe('AccountManager (account creation tests)', function () { it('should not create WebID if no username is given', (done) => { let subdomain = supertest('https://nicola.' + host) - let spkac = read('example_spkac.cnf') subdomain.post('/api/accounts/new') - .send('username=&spkac=' + spkac) + .send('username=&password=12345') + .expect(400, done) + }) + + it('should not create WebID if no password is given', (done) => { + let subdomain = supertest('https://nicola.' + host) + subdomain.post('/api/accounts/new') + .send('username=nicola&password=') .expect(400, done) }) it('should not create a WebID if it already exists', function (done) { var subdomain = supertest('https://nicola.' + host) - let spkac = read('example_spkac.cnf') subdomain.post('/api/accounts/new') - .send('username=nicola&spkac=' + spkac) - .expect(200) - .end((err) => { + .send('username=nicola&password=12345') + .expect(302) + .end((err, res) => { if (err) { return done(err) } subdomain.post('/api/accounts/new') - .send('username=nicola&spkac=' + spkac) + .send('username=nicola&password=12345') .expect(400) .end((err) => { done(err) @@ -127,10 +109,9 @@ describe('AccountManager (account creation tests)', function () { it('should create the default folders', function (done) { var subdomain = supertest('https://nicola.' + host) - let spkac = read('example_spkac.cnf') subdomain.post('/api/accounts/new') - .send('username=nicola&spkac=' + spkac) - .expect(200) + .send('username=nicola&password=12345') + .expect(302) .end(function (err) { if (err) { return done(err) @@ -155,14 +136,13 @@ describe('AccountManager (account creation tests)', function () { done(new Error('failed to create default files')) } }) - }) + }).timeout(20000) it('should link WebID to the root account', function (done) { var subdomain = supertest('https://nicola.' + host) - let spkac = read('example_spkac.cnf') subdomain.post('/api/accounts/new') - .send('username=nicola&spkac=' + spkac) - .expect(200) + .send('username=nicola&password=12345') + .expect(302) .end(function (err) { if (err) { return done(err) @@ -190,7 +170,7 @@ describe('AccountManager (account creation tests)', function () { } }) }) - }) + }).timeout(20000) it('should create a private settings container', function (done) { var subdomain = supertest('https://nicola.' + host) diff --git a/test/integration/account-creation-tls.js b/test/integration/account-creation-tls.js new file mode 100644 index 000000000..7b161b8dc --- /dev/null +++ b/test/integration/account-creation-tls.js @@ -0,0 +1,227 @@ +// const supertest = require('supertest') +// // Helper functions for the FS +// const $rdf = require('rdflib') +// +// const { rm, read } = require('../test-utils') +// const ldnode = require('../../index') +// const fs = require('fs-extra') +// const path = require('path') +// +// describe('AccountManager (TLS account creation tests)', function () { +// this.timeout(10000) +// process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +// +// var address = 'https://localhost:3457' +// var host = 'localhost:3457' +// var ldpHttpsServer +// let rootPath = path.join(__dirname, '../resources/accounts/') +// var ldp = ldnode.createServer({ +// root: rootPath, +// sslKey: path.join(__dirname, '../keys/key.pem'), +// sslCert: path.join(__dirname, '../keys/cert.pem'), +// auth: 'tls', +// webid: true, +// idp: true, +// strictOrigin: true +// }) +// +// before(function (done) { +// ldpHttpsServer = ldp.listen(3457, done) +// }) +// +// after(function () { +// if (ldpHttpsServer) ldpHttpsServer.close() +// fs.removeSync(path.join(rootPath, 'localhost/index.html')) +// fs.removeSync(path.join(rootPath, 'localhost/index.html.acl')) +// }) +// +// var server = supertest(address) +// +// it('should expect a 404 on GET /accounts', function (done) { +// server.get('/api/accounts') +// .expect(404, done) +// }) +// +// describe('accessing accounts', function () { +// it('should be able to access public file of an account', function (done) { +// var subdomain = supertest('https://tim.' + host) +// subdomain.get('/hello.html') +// .expect(200, done) +// }) +// it('should get 404 if root does not exist', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// subdomain.get('/') +// .set('Accept', 'text/turtle') +// .set('Origin', 'http://example.com') +// .expect(404) +// .expect('Access-Control-Allow-Origin', 'http://example.com') +// .expect('Access-Control-Allow-Credentials', 'true') +// .end(function (err, res) { +// done(err) +// }) +// }) +// }) +// +// describe('generating a certificate', () => { +// beforeEach(function () { +// rm('accounts/nicola.localhost') +// }) +// after(function () { +// rm('accounts/nicola.localhost') +// }) +// +// it('should generate a certificate if spkac is valid', (done) => { +// var spkac = read('example_spkac.cnf') +// var subdomain = supertest.agent('https://nicola.' + host) +// subdomain.post('/api/accounts/new') +// .send('username=nicola&spkac=' + spkac) +// .expect('Content-Type', /application\/x-x509-user-cert/) +// .expect(200, done) +// }) +// +// it('should not generate a certificate if spkac is not valid', (done) => { +// var subdomain = supertest('https://nicola.' + host) +// subdomain.post('/api/accounts/new') +// .send('username=nicola') +// .expect(200) +// .end((err) => { +// if (err) return done(err) +// +// subdomain.post('/api/accounts/cert') +// .send('username=nicola&spkac=') +// .expect(400, done) +// }) +// }) +// }) +// +// describe('creating an account with POST', function () { +// beforeEach(function () { +// rm('accounts/nicola.localhost') +// }) +// +// after(function () { +// rm('accounts/nicola.localhost') +// }) +// +// it('should not create WebID if no username is given', (done) => { +// let subdomain = supertest('https://nicola.' + host) +// let spkac = read('example_spkac.cnf') +// subdomain.post('/api/accounts/new') +// .send('username=&spkac=' + spkac) +// .expect(400, done) +// }) +// +// it('should not create a WebID if it already exists', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// let spkac = read('example_spkac.cnf') +// subdomain.post('/api/accounts/new') +// .send('username=nicola&spkac=' + spkac) +// .expect(200) +// .end((err) => { +// if (err) { +// return done(err) +// } +// subdomain.post('/api/accounts/new') +// .send('username=nicola&spkac=' + spkac) +// .expect(400) +// .end((err) => { +// done(err) +// }) +// }) +// }) +// +// it('should create the default folders', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// let spkac = read('example_spkac.cnf') +// subdomain.post('/api/accounts/new') +// .send('username=nicola&spkac=' + spkac) +// .expect(200) +// .end(function (err) { +// if (err) { +// return done(err) +// } +// var domain = host.split(':')[0] +// var card = read(path.join('accounts/nicola.' + domain, +// 'profile/card')) +// var cardAcl = read(path.join('accounts/nicola.' + domain, +// 'profile/card.acl')) +// var prefs = read(path.join('accounts/nicola.' + domain, +// 'settings/prefs.ttl')) +// var inboxAcl = read(path.join('accounts/nicola.' + domain, +// 'inbox/.acl')) +// var rootMeta = read(path.join('accounts/nicola.' + domain, '.meta')) +// var rootMetaAcl = read(path.join('accounts/nicola.' + domain, +// '.meta.acl')) +// +// if (domain && card && cardAcl && prefs && inboxAcl && rootMeta && +// rootMetaAcl) { +// done() +// } else { +// done(new Error('failed to create default files')) +// } +// }) +// }) +// +// it('should link WebID to the root account', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// let spkac = read('example_spkac.cnf') +// subdomain.post('/api/accounts/new') +// .send('username=nicola&spkac=' + spkac) +// .expect(200) +// .end(function (err) { +// if (err) { +// return done(err) +// } +// subdomain.get('/.meta') +// .expect(200) +// .end(function (err, data) { +// if (err) { +// return done(err) +// } +// var graph = $rdf.graph() +// $rdf.parse( +// data.text, +// graph, +// 'https://nicola.' + host + '/.meta', +// 'text/turtle') +// var statements = graph.statementsMatching( +// undefined, +// $rdf.sym('http://www.w3.org/ns/solid/terms#account'), +// undefined) +// if (statements.length === 1) { +// done() +// } else { +// done(new Error('missing link to WebID of account')) +// } +// }) +// }) +// }) +// +// it('should create a private settings container', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// subdomain.head('/settings/') +// .expect(401) +// .end(function (err) { +// done(err) +// }) +// }) +// +// it('should create a private prefs file in the settings container', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// subdomain.head('/inbox/prefs.ttl') +// .expect(401) +// .end(function (err) { +// done(err) +// }) +// }) +// +// it('should create a private inbox container', function (done) { +// var subdomain = supertest('https://nicola.' + host) +// subdomain.head('/inbox/') +// .expect(401) +// .end(function (err) { +// done(err) +// }) +// }) +// }) +// }) diff --git a/test/integration/account-manager.js b/test/integration/account-manager.js new file mode 100644 index 000000000..264aa8c27 --- /dev/null +++ b/test/integration/account-manager.js @@ -0,0 +1,119 @@ +'use strict' + +const path = require('path') +const fs = require('fs-extra') +const chai = require('chai') +const expect = chai.expect +chai.should() + +const LDP = require('../../lib/ldp') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') + +const testAccountsDir = path.join(__dirname, '../resources/accounts') +const accountTemplatePath = path.join(__dirname, '../../default-templates/new-account') + +var host + +beforeEach(() => { + host = SolidHost.from({ serverUri: 'https://example.com' }) +}) + +afterEach(() => { + fs.removeSync(path.join(__dirname, '../resources/accounts/alice.example.com')) +}) + +describe('AccountManager', () => { + describe('accountExists()', () => { + let host = SolidHost.from({ serverUri: 'https://localhost' }) + + describe('in multi user mode', () => { + let multiUser = true + let store = new LDP({ root: testAccountsDir, idp: multiUser }) + let options = { multiUser, store, host } + let accountManager = AccountManager.from(options) + + it('resolves to true if a directory for the account exists in root', () => { + // Note: test/resources/accounts/tim.localhost/ exists in this repo + return accountManager.accountExists('tim') + .then(exists => { + expect(exists).to.be.true + }) + }) + + it('resolves to false if a directory for the account does not exist', () => { + // Note: test/resources/accounts/alice.localhost/ does NOT exist + return accountManager.accountExists('alice') + .then(exists => { + expect(exists).to.be.false + }) + }) + }) + + describe('in single user mode', () => { + let multiUser = false + + it('resolves to true if root .acl exists in root storage', () => { + let store = new LDP({ + root: path.join(testAccountsDir, 'tim.localhost'), + idp: multiUser + }) + let options = { multiUser, store, host } + let accountManager = AccountManager.from(options) + + return accountManager.accountExists() + .then(exists => { + expect(exists).to.be.true + }) + }) + + it('resolves to false if root .acl does not exist in root storage', () => { + let store = new LDP({ + root: testAccountsDir, + idp: multiUser + }) + let options = { multiUser, store, host } + let accountManager = AccountManager.from(options) + + return accountManager.accountExists() + .then(exists => { + expect(exists).to.be.false + }) + }) + }) + }) + + describe('createAccountFor()', () => { + it('should create an account directory', () => { + let multiUser = true + let store = new LDP({ root: testAccountsDir, idp: multiUser }) + let options = { host, multiUser, store, accountTemplatePath } + let accountManager = AccountManager.from(options) + + let userData = { + username: 'alice', + email: 'alice@example.com', + name: 'Alice Q.' + } + let userAccount = accountManager.userAccountFrom(userData) + + let accountDir = accountManager.accountDirFor('alice') + + return accountManager.createAccountFor(userAccount) + .then(() => { + return accountManager.accountExists('alice') + }) + .then(found => { + expect(found).to.be.true + }) + .then(() => { + let profile = fs.readFileSync(path.join(accountDir, '/profile/card'), 'utf8') + expect(profile).to.include('"Alice Q."') + + let rootAcl = fs.readFileSync(path.join(accountDir, '.acl'), 'utf8') + expect(rootAcl).to.include('') + }) + }) + }) +}) diff --git a/test/integration/account-template.js b/test/integration/account-template.js new file mode 100644 index 000000000..ff5cdcc94 --- /dev/null +++ b/test/integration/account-template.js @@ -0,0 +1,58 @@ +'use strict' + +const path = require('path') +const fs = require('fs-extra') +const chai = require('chai') +const expect = chai.expect +const sinonChai = require('sinon-chai') +chai.use(sinonChai) +chai.should() + +const AccountTemplate = require('../../lib/models/account-template') + +const templatePath = path.join(__dirname, '../../default-templates/new-account') +const accountPath = path.join(__dirname, '../resources/new-account') + +describe('AccountTemplate', () => { + beforeEach(() => { + fs.removeSync(accountPath) + }) + + afterEach(() => { + fs.removeSync(accountPath) + }) + + describe('copy()', () => { + it('should copy a directory', () => { + return AccountTemplate.copyTemplateDir(templatePath, accountPath) + .then(() => { + let rootAcl = fs.readFileSync(path.join(accountPath, '.acl'), 'utf8') + expect(rootAcl).to.exist + }) + }) + }) + + describe('processAccount()', () => { + it('should process all the files in an account', () => { + let substitutions = { + webId: 'https://alice.example.com/#me', + email: 'alice@example.com', + name: 'Alice Q.' + } + let template = new AccountTemplate({ substitutions }) + + return AccountTemplate.copyTemplateDir(templatePath, accountPath) + .then(() => { + return template.processAccount(accountPath) + }) + .then(() => { + let profile = fs.readFileSync(path.join(accountPath, '/profile/card'), 'utf8') + expect(profile).to.include('"Alice Q."') + + let rootAcl = fs.readFileSync(path.join(accountPath, '.acl'), 'utf8') + expect(rootAcl).to.include('') + }) + }) + }) +}) diff --git a/test/integration/acl-oidc.js b/test/integration/acl-oidc.js new file mode 100644 index 000000000..b9f8207f8 --- /dev/null +++ b/test/integration/acl-oidc.js @@ -0,0 +1,655 @@ +const assert = require('chai').assert +const fs = require('fs-extra') +const request = require('request') +const path = require('path') +const rm = require('../test-utils').rm + +const ldnode = require('../../index') + +const port = 7777 +const serverUri = `https://localhost:7777` +const rootPath = path.join(__dirname, '../resources/accounts-acl') +const dbPath = path.join(rootPath, 'db') +const configPath = path.join(rootPath, 'config') + +const user1 = 'https://tim.localhost:7777/profile/card#me' +const timAccountUri = 'https://tim.localhost:7777' +const user2 = 'https://nicola.localhost:7777/profile/card#me' + +const userCredentials = { + user1: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkFWUzVlZk5pRUVNIn0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3Nzc3Iiwic3ViIjoiaHR0cHM6Ly90aW0ubG9jYWxob3N0Ojc3NzcvcHJvZmlsZS9jYXJkI21lIiwiYXVkIjoiN2YxYmU5YWE0N2JiMTM3MmIzYmM3NWU5MWRhMzUyYjQiLCJleHAiOjc3OTkyMjkwMDksImlhdCI6MTQ5MjAyOTAwOSwianRpIjoiZWY3OGQwYjY3ZWRjNzJhMSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUifQ.H9lxCbNc47SfIq3hhHnj48BE-YFnvhCfDH9Jc4PptApTEip8sVj0E_u704K_huhNuWBvuv3cDRDGYZM7CuLnzgJG1BI75nXR9PYAJPK9Ketua2KzIrftNoyKNamGqkoCKFafF4z_rsmtXQ5u1_60SgWRcouXMpcHnnDqINF1JpvS21xjE_LbJ6qgPEhu3rRKcv1hpRdW9dRvjtWb9xu84bAjlRuT02lyDBHgj2utxpE_uqCbj48qlee3GoqWpGkSS-vJ6JA0aWYgnyv8fQsxf9rpdFNzKRoQO6XYMy6niEKj8aKgxjaUlpoGGJ5XtVLHH8AGwjYXR8iznYzJvEcB7Q', + user2: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkFWUzVlZk5pRUVNIn0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3Nzc3Iiwic3ViIjoiaHR0cHM6Ly9uaWNvbGEubG9jYWxob3N0Ojc3NzcvcHJvZmlsZS9jYXJkI21lIiwiYXVkIjoiN2YxYmU5YWE0N2JiMTM3MmIzYmM3NWU5MWRhMzUyYjQiLCJleHAiOjc3OTkyMjkwMDksImlhdCI6MTQ5MjAyOTAwOSwianRpIjoiMmQwOTJlZGVkOWI5YTQ5ZSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUifQ.qs-_pZPZZzaK_pIOQr-T3yMxVPo1Z5R-TwIi_a4Q4Arudu2s9VkoPmsfsCeVc22i6I1uLiaRe_9qROpXd-Oiy0dsMMEtqyQWcc0zxp3RYQs99sAi4pTPOsTjtJwsMRJp4n8nx_TWQ7mS1grZEdSLr53v-2QqTZXVW8cBu4vQ0slXWsKsuaySk-hCMnxk7vHj70uFpuKRjx4CBHkEWXooEyXgcmS8QR-d_peq8Ldkq1Bez4SAQ9sy_4UVaIWoLRqA7gr0Grh7OTHZNdYV_NJoH0mnbCuyS5N5YEI8QuUzuYlSNhgZ_cZ3j1uqw_fs8SIHFtWMghdnT2JdRKUFfn4-vA' +} + +const argv = { + root: rootPath, + serverUri, + dbPath, + port, + configPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + webid: true, + idp: true, + auth: 'oidc', + strictOrigin: true, + host: { serverUri } +} + +describe('ACL HTTP', function () { + this.timeout(10000) + + var ldp, ldpHttpsServer + + before(done => { + ldp = ldnode.createServer(argv) + ldpHttpsServer = ldp.listen(port, done) + }) + + after(() => { + if (ldpHttpsServer) ldpHttpsServer.close() + fs.removeSync(path.join(rootPath, 'index.html')) + fs.removeSync(path.join(rootPath, 'index.html.acl')) + }) + + const origin1 = 'http://example.org/' + const origin2 = 'http://example.com/' + + function createOptions (path, user) { + const options = { + url: timAccountUri + path, + headers: { + accept: 'text/turtle' + } + } + if (user) { + let accessToken = userCredentials[user] + options.headers['Authorization'] = 'Bearer ' + accessToken + } + + return options + } + + describe('no ACL', function () { + it('should return 403 for any resource', function (done) { + var options = createOptions('/no-acl/', 'user1') + request(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('should have `User` set in the Response Header', function (done) { + var options = createOptions('/no-acl/', 'user1') + request(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + }) + + describe('empty .acl', function () { + describe('with no defaultForNew in parent path', function () { + it('should give no access', function (done) { + var options = createOptions('/empty-acl/test-folder', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('should not let edit the .acl', function (done) { + var options = createOptions('/empty-acl/.acl', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('should not let read the .acl', function (done) { + var options = createOptions('/empty-acl/.acl', 'user1') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + }) + describe('with defaultForNew in parent path', function () { + before(() => { + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/another-empty-folder/test-file.acl') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder/test-file') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-file') + rm('/accounts-acl/tim.localhost/write-acl/test-file') + rm('/accounts-acl/tim.localhost/write-acl/test-file.acl') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder2/') + }) + + after(() => { + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder/') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder2/') + }) + + it('should allow creation of a container', function (done) { + var options = createOptions('/write-acl/empty-acl/test-folder2/', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('should allow creation of new files', function (done) { + var options = createOptions('/write-acl/empty-acl/test-file', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('should allow creation of new files in deeper paths', function (done) { + var options = createOptions('/write-acl/empty-acl/test-folder/test-file', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('Should create empty acl file', function (done) { + var options = createOptions('/write-acl/empty-acl/another-empty-folder/test-file.acl', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('should return text/turtle for the acl file', function (done) { + var options = createOptions('/write-acl/.acl', 'user1') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + assert.match(response.headers['content-type'], /text\/turtle/) + done() + }) + }) + it('should create test file', function (done) { + var options = createOptions('/write-acl/test-file', 'user1') + options.body = ' .' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it("should create test file's acl file", function (done) { + var options = createOptions('/write-acl/test-file.acl', 'user1') + options.body = '' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it("should access test file's acl file", function (done) { + var options = createOptions('/write-acl/test-file.acl', 'user1') + request.get(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + assert.match(response.headers['content-type'], /text\/turtle/) + done() + }) + }) + + after(function () { + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/another-empty-folder/test-file.acl') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-folder/test-file') + rm('/accounts-acl/tim.localhost/write-acl/empty-acl/test-file') + rm('/accounts-acl/tim.localhost/write-acl/test-file') + rm('/accounts-acl/tim.localhost/write-acl/test-file.acl') + }) + }) + }) + + describe('Origin', function () { + before(function () { + rm('/accounts-acl/tim.localhost/origin/test-folder/.acl') + }) + + it('should PUT new ACL file', function (done) { + var options = createOptions('/origin/test-folder/.acl', 'user1') + options.body = '<#Owner> a ;\n' + + ' ;\n' + + ' <' + user1 + '>;\n' + + ' <' + origin1 + '>;\n' + + ' , , .\n' + + '<#Public> a ;\n' + + ' <./>;\n' + + ' ;\n' + + ' <' + origin1 + '>;\n' + + ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + // TODO triple header + // TODO user header + }) + }) + it('user1 should be able to access test directory', function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin1 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to access to test directory when origin is valid', + function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin1 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be denied access to test directory when origin is invalid', + function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin2 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('agent should be able to access test directory', function (done) { + var options = createOptions('/origin/test-folder/') + options.headers.origin = origin1 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('agent should be able to access to test directory when origin is valid', + function (done) { + var options = createOptions('/origin/test-folder/', 'user1') + options.headers.origin = origin1 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('agent should be denied access to test directory when origin is invalid', + function (done) { + var options = createOptions('/origin/test-folder/') + options.headers.origin = origin2 + + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + + after(function () { + rm('/accounts-acl/tim.localhost/origin/test-folder/.acl') + }) + }) + + describe('Read-only', function () { + var body = fs.readFileSync(path.join(rootPath, 'tim.localhost/read-acl/.acl')) + it('user1 should be able to access ACL file', function (done) { + var options = createOptions('/read-acl/.acl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to access test directory', function (done) { + var options = createOptions('/read-acl/', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to modify ACL file', function (done) { + var options = createOptions('/read-acl/.acl', 'user1') + options.body = body + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 204) + done() + }) + }) + it('user2 should be able to access test directory', function (done) { + var options = createOptions('/read-acl/', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user2 should not be able to access ACL file', function (done) { + var options = createOptions('/read-acl/.acl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('user2 should not be able to modify ACL file', function (done) { + var options = createOptions('/read-acl/.acl', 'user2') + options.body = ' .' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('agent should be able to access test direcotory', function (done) { + var options = createOptions('/read-acl/') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('agent should not be able to modify ACL file', function (done) { + var options = createOptions('/read-acl/.acl') + options.body = ' .' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + }) + + describe('Append-only', function () { + // var body = fs.readFileSync(__dirname + '/resources/append-acl/abc.ttl.acl') + it("user1 should be able to access test file's ACL file", function (done) { + var options = createOptions('/append-acl/abc.ttl.acl', 'user1') + request.head(options, function (error, response) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to PATCH a resource', function (done) { + var options = createOptions('/append-inherited/test.ttl', 'user1') + options.body = 'INSERT DATA { :test :hello 456 .}' + options.headers['content-type'] = 'application/sparql-update' + request.patch(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to access test file', function (done) { + var options = createOptions('/append-acl/abc.ttl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + // TODO POST instead of PUT + it('user1 should be able to modify test file', function (done) { + var options = createOptions('/append-acl/abc.ttl', 'user1') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 204) + done() + }) + }) + it("user2 should not be able to access test file's ACL file", function (done) { + var options = createOptions('/append-acl/abc.ttl.acl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('user2 should not be able to access test file', function (done) { + var options = createOptions('/append-acl/abc.ttl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('user2 (with append permission) cannot use PUT to append', function (done) { + var options = createOptions('/append-acl/abc.ttl', 'user2') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('agent should not be able to access test file', function (done) { + var options = createOptions('/append-acl/abc.ttl') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + it('agent (with append permissions) should not PUT', function (done) { + var options = createOptions('/append-acl/abc.ttl') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + after(function () { + rm('/accounts-acl/tim.localhost/append-inherited/test.ttl') + }) + }) + + describe('Restricted', function () { + var body = '<#Owner> a ;\n' + + ' <./abc2.ttl>;\n' + + ' <' + user1 + '>;\n' + + ' , , .\n' + + '<#Restricted> a ;\n' + + ' <./abc2.ttl>;\n' + + ' <' + user2 + '>;\n' + + ' , .\n' + it("user1 should be able to modify test file's ACL file", function (done) { + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + options.body = body + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 204) + done() + }) + }) + it("user1 should be able to access test file's ACL file", function (done) { + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to access test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to modify test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl', 'user1') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 204) + done() + }) + }) + it('user2 should be able to access test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it("user2 should not be able to access test file's ACL file", function (done) { + var options = createOptions('/append-acl/abc2.ttl.acl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('user2 should be able to modify test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl', 'user2') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 204) + done() + }) + }) + it('agent should not be able to access test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + it('agent should not be able to modify test file', function (done) { + var options = createOptions('/append-acl/abc2.ttl') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + }) + + describe('defaultForNew', function () { + before(function () { + rm('/accounts-acl/tim.localhost/write-acl/default-for-new/.acl') + rm('/accounts-acl/tim.localhost/write-acl/default-for-new/test-file.ttl') + }) + + var body = '<#Owner> a ;\n' + + ' <./>;\n' + + ' <' + user1 + '>;\n' + + ' <./>;\n' + + ' , , .\n' + + '<#Default> a ;\n' + + ' <./>;\n' + + ' <./>;\n' + + ' ;\n' + + ' .\n' + it("user1 should be able to modify test directory's ACL file", function (done) { + var options = createOptions('/write-acl/default-for-new/.acl', 'user1') + options.body = body + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it("user1 should be able to access test direcotory's ACL file", function (done) { + var options = createOptions('/write-acl/default-for-new/.acl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user1 should be able to create new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 201) + done() + }) + }) + it('user1 should be able to access new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it("user2 should not be able to access test direcotory's ACL file", function (done) { + var options = createOptions('/write-acl/default-for-new/.acl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('user2 should be able to access new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('user2 should not be able to modify new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 403) + done() + }) + }) + it('agent should be able to access new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl') + request.head(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) + it('agent should not be able to modify new test file', function (done) { + var options = createOptions('/write-acl/default-for-new/test-file.ttl') + options.body = ' .\n' + request.put(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + done() + }) + }) + + after(function () { + rm('/accounts-acl/tim.localhost/write-acl/default-for-new/.acl') + rm('/accounts-acl/tim.localhost/write-acl/default-for-new/test-file.ttl') + }) + }) +}) diff --git a/test/acl.js b/test/integration/acl-tls.js similarity index 76% rename from test/acl.js rename to test/integration/acl-tls.js index 75586e4db..d9d1f8dd5 100644 --- a/test/acl.js +++ b/test/integration/acl-tls.js @@ -1,16 +1,21 @@ var assert = require('chai').assert -var fs = require('fs') +var fs = require('fs-extra') var $rdf = require('rdflib') var request = require('request') var path = require('path') +/** + * Note: this test suite requires an internet connection, since it actually + * uses remote accounts https://user1.databox.me and https://user2.databox.me + */ + // Helper functions for the FS -var rm = require('./test-utils').rm +var rm = require('../test-utils').rm // var write = require('./test-utils').write // var cp = require('./test-utils').cp // var read = require('./test-utils').read -var ldnode = require('../index') +var ldnode = require('../../index') var ns = require('solid-namespace')($rdf) describe('ACL HTTP', function () { @@ -18,15 +23,16 @@ describe('ACL HTTP', function () { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' var address = 'https://localhost:3456/test/' - + let rootPath = path.join(__dirname, '../resources') var ldpHttpsServer var ldp = ldnode.createServer({ mount: '/test', - root: path.join(__dirname, '/resources'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), + root: rootPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), webid: true, - strictOrigin: true + strictOrigin: true, + auth: 'tls' }) before(function (done) { @@ -35,12 +41,14 @@ describe('ACL HTTP', function () { after(function () { if (ldpHttpsServer) ldpHttpsServer.close() + fs.removeSync(path.join(rootPath, 'index.html')) + fs.removeSync(path.join(rootPath, 'index.html.acl')) }) var aclExtension = '.acl' var metaExtension = '.meta' - var testDir = 'acl/testDir' + var testDir = 'acl-tls/testDir' var testDirAclFile = testDir + '/' + aclExtension var testDirMetaFile = testDir + '/' + metaExtension @@ -58,12 +66,12 @@ describe('ACL HTTP', function () { var user2 = 'https://user2.databox.me/profile/card#me' var userCredentials = { user1: { - cert: fs.readFileSync(path.join(__dirname, '/keys/user1-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user1-key.pem')) + cert: fs.readFileSync(path.join(__dirname, '../keys/user1-cert.pem')), + key: fs.readFileSync(path.join(__dirname, '../keys/user1-key.pem')) }, user2: { - cert: fs.readFileSync(path.join(__dirname, '/keys/user2-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user2-key.pem')) + cert: fs.readFileSync(path.join(__dirname, '../keys/user2-cert.pem')), + key: fs.readFileSync(path.join(__dirname, '../keys/user2-key.pem')) } } @@ -82,18 +90,36 @@ describe('ACL HTTP', function () { describe('no ACL', function () { it('should return 403 for any resource', function (done) { - var options = createOptions('/acl/no-acl/', 'user1') + var options = createOptions('/acl-tls/no-acl/', 'user1') request(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) done() }) }) + it('should have `User` set in the Response Header', function (done) { - var options = createOptions('/acl/no-acl/', 'user1') + var options = createOptions('/acl-tls/no-acl/', 'user1') request(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) + assert.equal(response.headers['user'], + 'https://user1.databox.me/profile/card#me') + done() + }) + }) + + it('should return a 401 and WWW-Authenticate header without credentials', (done) => { + let options = { + url: address + '/acl-tls/no-acl/', + headers: { accept: 'text/turtle' } + } + + request(options, (error, response, body) => { + assert.equal(error, null) + assert.equal(response.statusCode, 401) + assert.equal(response.headers['www-authenticate'], + 'WebID-TLS realm="https://localhost:8443"') done() }) }) @@ -102,7 +128,7 @@ describe('ACL HTTP', function () { describe('empty .acl', function () { describe('with no defaultForNew in parent path', function () { it('should give no access', function (done) { - var options = createOptions('/acl/empty-acl/test-folder', 'user1') + var options = createOptions('/acl-tls/empty-acl/test-folder', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -111,7 +137,7 @@ describe('ACL HTTP', function () { }) }) it('should not let edit the .acl', function (done) { - var options = createOptions('/acl/empty-acl/.acl', 'user1') + var options = createOptions('/acl-tls/empty-acl/.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -123,7 +149,7 @@ describe('ACL HTTP', function () { }) }) it('should not let read the .acl', function (done) { - var options = createOptions('/acl/empty-acl/.acl', 'user1') + var options = createOptions('/acl-tls/empty-acl/.acl', 'user1') options.headers = { accept: 'text/turtle' } @@ -135,25 +161,31 @@ describe('ACL HTTP', function () { }) }) describe('with defaultForNew in parent path', function () { - before(function () { - rm('/acl/write-acl/empty-acl/another-empty-folder/test-file.acl') - rm('/acl/write-acl/empty-acl/test-folder/test-file') - rm('/acl/write-acl/empty-acl/test-file') - rm('/acl/write-acl/test-file') - rm('/acl/write-acl/test-file.acl') + before(() => { + rm('/acl-tls/write-acl/empty-acl/another-empty-folder/test-file.acl') + rm('/acl-tls/write-acl/empty-acl/test-folder/test-file') + rm('/acl-tls/write-acl/empty-acl/test-file') + rm('/acl-tls/write-acl/test-file') + rm('/acl-tls/write-acl/test-file.acl') + rm('/acl-tls/write-acl/empty-acl/test-folder2/') }) - it('should fail to create a container', function (done) { - var options = createOptions('/acl/write-acl/empty-acl/test-folder/', 'user1') + after(() => { + rm('/acl-tls/write-acl/empty-acl/test-folder/') + rm('/acl-tls/write-acl/empty-acl/test-folder2/') + }) + + it('should allow creation of a container', function (done) { + var options = createOptions('/acl-tls/write-acl/empty-acl/test-folder2/', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 409) + assert.equal(response.statusCode, 201) done() }) }) it('should allow creation of new files', function (done) { - var options = createOptions('/acl/write-acl/empty-acl/test-file', 'user1') + var options = createOptions('/acl-tls/write-acl/empty-acl/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -162,7 +194,7 @@ describe('ACL HTTP', function () { }) }) it('should allow creation of new files in deeper paths', function (done) { - var options = createOptions('/acl/write-acl/empty-acl/test-folder/test-file', 'user1') + var options = createOptions('/acl-tls/write-acl/empty-acl/test-folder/test-file', 'user1') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -171,7 +203,7 @@ describe('ACL HTTP', function () { }) }) it('Should create empty acl file', function (done) { - var options = createOptions('/acl/write-acl/empty-acl/another-empty-folder/test-file.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/empty-acl/another-empty-folder/test-file.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -183,7 +215,7 @@ describe('ACL HTTP', function () { }) }) it('should return text/turtle for the acl file', function (done) { - var options = createOptions('/acl/write-acl/.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/.acl', 'user1') options.headers = { accept: 'text/turtle' } @@ -195,7 +227,7 @@ describe('ACL HTTP', function () { }) }) it('should create test file', function (done) { - var options = createOptions('/acl/write-acl/test-file', 'user1') + var options = createOptions('/acl-tls/write-acl/test-file', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -207,7 +239,7 @@ describe('ACL HTTP', function () { }) }) it("should create test file's acl file", function (done) { - var options = createOptions('/acl/write-acl/test-file.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/test-file.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -219,7 +251,7 @@ describe('ACL HTTP', function () { }) }) it("should access test file's acl file", function (done) { - var options = createOptions('/acl/write-acl/test-file.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/test-file.acl', 'user1') options.headers = { accept: 'text/turtle' } @@ -232,27 +264,27 @@ describe('ACL HTTP', function () { }) after(function () { - rm('/acl/write-acl/empty-acl/another-empty-folder/test-file.acl') - rm('/acl/write-acl/empty-acl/test-folder/test-file') - rm('/acl/write-acl/empty-acl/test-file') - rm('/acl/write-acl/test-file') - rm('/acl/write-acl/test-file.acl') + rm('/acl-tls/write-acl/empty-acl/another-empty-folder/test-file.acl') + rm('/acl-tls/write-acl/empty-acl/test-folder/test-file') + rm('/acl-tls/write-acl/empty-acl/test-file') + rm('/acl-tls/write-acl/test-file') + rm('/acl-tls/write-acl/test-file.acl') }) }) }) describe('Origin', function () { before(function () { - rm('acl/origin/test-folder/.acl') + rm('acl-tls/origin/test-folder/.acl') }) it('should PUT new ACL file', function (done) { - var options = createOptions('/acl/origin/test-folder/.acl', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } options.body = '<#Owner> a ;\n' + - ' ;\n' + + ' ;\n' + ' <' + user1 + '>;\n' + ' <' + origin1 + '>;\n' + ' , , .\n' + @@ -265,12 +297,12 @@ describe('ACL HTTP', function () { assert.equal(error, null) assert.equal(response.statusCode, 201) done() - // TODO triple header - // TODO user header + // TODO triple header + // TODO user header }) }) it('user1 should be able to access test directory', function (done) { - var options = createOptions('/acl/origin/test-folder/', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin1 request.head(options, function (error, response, body) { @@ -281,7 +313,7 @@ describe('ACL HTTP', function () { }) it('user1 should be able to access to test directory when origin is valid', function (done) { - var options = createOptions('/acl/origin/test-folder/', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin1 request.head(options, function (error, response, body) { @@ -292,7 +324,7 @@ describe('ACL HTTP', function () { }) it('user1 should be denied access to test directory when origin is invalid', function (done) { - var options = createOptions('/acl/origin/test-folder/', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin2 request.head(options, function (error, response, body) { @@ -302,7 +334,7 @@ describe('ACL HTTP', function () { }) }) it('agent should be able to access test directory', function (done) { - var options = createOptions('/acl/origin/test-folder/') + var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin1 request.head(options, function (error, response, body) { @@ -313,7 +345,7 @@ describe('ACL HTTP', function () { }) it('agent should be able to access to test directory when origin is valid', function (done) { - var options = createOptions('/acl/origin/test-folder/', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/', 'user1') options.headers.origin = origin1 request.head(options, function (error, response, body) { @@ -324,7 +356,7 @@ describe('ACL HTTP', function () { }) it('agent should be denied access to test directory when origin is invalid', function (done) { - var options = createOptions('/acl/origin/test-folder/') + var options = createOptions('/acl-tls/origin/test-folder/') options.headers.origin = origin2 request.head(options, function (error, response, body) { @@ -335,14 +367,14 @@ describe('ACL HTTP', function () { }) after(function () { - rm('acl/origin/test-folder/.acl') + rm('acl-tls/origin/test-folder/.acl') }) }) describe('Read-only', function () { - var body = fs.readFileSync(path.join(__dirname, '/resources/acl/read-acl/.acl')) + var body = fs.readFileSync(path.join(__dirname, '../resources/acl-tls/read-acl/.acl')) it('user1 should be able to access ACL file', function (done) { - var options = createOptions('/acl/read-acl/.acl', 'user1') + var options = createOptions('/acl-tls/read-acl/.acl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -350,7 +382,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to access test directory', function (done) { - var options = createOptions('/acl/read-acl/', 'user1') + var options = createOptions('/acl-tls/read-acl/', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -358,19 +390,19 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to modify ACL file', function (done) { - var options = createOptions('/acl/read-acl/.acl', 'user1') + var options = createOptions('/acl-tls/read-acl/.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) it('user2 should be able to access test directory', function (done) { - var options = createOptions('/acl/read-acl/', 'user2') + var options = createOptions('/acl-tls/read-acl/', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -378,7 +410,7 @@ describe('ACL HTTP', function () { }) }) it('user2 should not be able to access ACL file', function (done) { - var options = createOptions('/acl/read-acl/.acl', 'user2') + var options = createOptions('/acl-tls/read-acl/.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -386,7 +418,7 @@ describe('ACL HTTP', function () { }) }) it('user2 should not be able to modify ACL file', function (done) { - var options = createOptions('/acl/read-acl/.acl', 'user2') + var options = createOptions('/acl-tls/read-acl/.acl', 'user2') options.headers = { 'content-type': 'text/turtle' } @@ -398,7 +430,7 @@ describe('ACL HTTP', function () { }) }) it('agent should be able to access test direcotory', function (done) { - var options = createOptions('/acl/read-acl/') + var options = createOptions('/acl-tls/read-acl/') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -406,7 +438,7 @@ describe('ACL HTTP', function () { }) }) it('agent should not be able to modify ACL file', function (done) { - var options = createOptions('/acl/read-acl/.acl') + var options = createOptions('/acl-tls/read-acl/.acl') options.headers = { 'content-type': 'text/turtle' } @@ -455,9 +487,9 @@ describe('ACL HTTP', function () { }) describe('Append-only', function () { - // var body = fs.readFileSync(__dirname + '/resources/acl/append-acl/abc.ttl.acl') + // var body = fs.readFileSync(__dirname + '/resources/acl-tls/append-acl/abc.ttl.acl') it("user1 should be able to access test file's ACL file", function (done) { - var options = createOptions('/acl/append-acl/abc.ttl.acl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc.ttl.acl', 'user1') request.head(options, function (error, response) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -465,7 +497,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to PATCH a resource', function (done) { - var options = createOptions('/acl/append-inherited/test.ttl', 'user1') + var options = createOptions('/acl-tls/append-inherited/test.ttl', 'user1') options.headers = { 'content-type': 'application/sparql-update' } @@ -477,7 +509,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc.ttl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -486,19 +518,19 @@ describe('ACL HTTP', function () { }) // TODO POST instead of PUT it('user1 should be able to modify test file', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc.ttl', 'user1') options.headers = { 'content-type': 'text/turtle' } options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) it("user2 should not be able to access test file's ACL file", function (done) { - var options = createOptions('/acl/append-acl/abc.ttl.acl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc.ttl.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -506,7 +538,7 @@ describe('ACL HTTP', function () { }) }) it('user2 should not be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc.ttl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -514,7 +546,7 @@ describe('ACL HTTP', function () { }) }) it('user2 (with append permission) cannot use PUT to append', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc.ttl', 'user2') options.headers = { 'content-type': 'text/turtle' } @@ -526,7 +558,7 @@ describe('ACL HTTP', function () { }) }) it('agent should not be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl') + var options = createOptions('/acl-tls/append-acl/abc.ttl') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) @@ -534,7 +566,7 @@ describe('ACL HTTP', function () { }) }) it('agent (with append permissions) should not PUT', function (done) { - var options = createOptions('/acl/append-acl/abc.ttl') + var options = createOptions('/acl-tls/append-acl/abc.ttl') options.headers = { 'content-type': 'text/turtle' } @@ -546,7 +578,7 @@ describe('ACL HTTP', function () { }) }) after(function () { - rm('acl/append-inherited/test.ttl') + rm('acl-tls/append-inherited/test.ttl') }) }) @@ -560,19 +592,19 @@ describe('ACL HTTP', function () { ' <' + user2 + '>;\n' + ' , .\n' it("user1 should be able to modify test file's ACL file", function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl.acl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc2.ttl.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) it("user1 should be able to access test file's ACL file", function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl.acl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc2.ttl.acl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -580,7 +612,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc2.ttl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -588,19 +620,19 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to modify test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl', 'user1') + var options = createOptions('/acl-tls/append-acl/abc2.ttl', 'user1') options.headers = { 'content-type': 'text/turtle' } options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) it('user2 should be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc2.ttl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -608,7 +640,7 @@ describe('ACL HTTP', function () { }) }) it("user2 should not be able to access test file's ACL file", function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl.acl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc2.ttl.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -616,19 +648,19 @@ describe('ACL HTTP', function () { }) }) it('user2 should be able to modify test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl', 'user2') + var options = createOptions('/acl-tls/append-acl/abc2.ttl', 'user2') options.headers = { 'content-type': 'text/turtle' } options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) it('agent should not be able to access test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl') + var options = createOptions('/acl-tls/append-acl/abc2.ttl') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 401) @@ -636,7 +668,7 @@ describe('ACL HTTP', function () { }) }) it('agent should not be able to modify test file', function (done) { - var options = createOptions('/acl/append-acl/abc2.ttl') + var options = createOptions('/acl-tls/append-acl/abc2.ttl') options.headers = { 'content-type': 'text/turtle' } @@ -682,7 +714,7 @@ describe('ACL HTTP', function () { options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) @@ -703,7 +735,7 @@ describe('ACL HTTP', function () { options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) - assert.equal(response.statusCode, 201) + assert.equal(response.statusCode, 204) done() }) }) @@ -783,8 +815,8 @@ describe('ACL HTTP', function () { describe('defaultForNew', function () { before(function () { - rm('/acl/write-acl/default-for-new/.acl') - rm('/acl/write-acl/default-for-new/test-file.ttl') + rm('/acl-tls/write-acl/default-for-new/.acl') + rm('/acl-tls/write-acl/default-for-new/test-file.ttl') }) var body = '<#Owner> a ;\n' + @@ -798,7 +830,7 @@ describe('ACL HTTP', function () { ' ;\n' + ' .\n' it("user1 should be able to modify test directory's ACL file", function (done) { - var options = createOptions('/acl/write-acl/default-for-new/.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/default-for-new/.acl', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -810,7 +842,7 @@ describe('ACL HTTP', function () { }) }) it("user1 should be able to access test direcotory's ACL file", function (done) { - var options = createOptions('/acl/write-acl/default-for-new/.acl', 'user1') + var options = createOptions('/acl-tls/write-acl/default-for-new/.acl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -818,7 +850,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to create new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl', 'user1') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl', 'user1') options.headers = { 'content-type': 'text/turtle' } @@ -830,7 +862,7 @@ describe('ACL HTTP', function () { }) }) it('user1 should be able to access new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl', 'user1') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl', 'user1') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -838,7 +870,7 @@ describe('ACL HTTP', function () { }) }) it("user2 should not be able to access test direcotory's ACL file", function (done) { - var options = createOptions('/acl/write-acl/default-for-new/.acl', 'user2') + var options = createOptions('/acl-tls/write-acl/default-for-new/.acl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -846,7 +878,7 @@ describe('ACL HTTP', function () { }) }) it('user2 should be able to access new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl', 'user2') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl', 'user2') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -854,7 +886,7 @@ describe('ACL HTTP', function () { }) }) it('user2 should not be able to modify new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl', 'user2') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl', 'user2') options.headers = { 'content-type': 'text/turtle' } @@ -866,7 +898,7 @@ describe('ACL HTTP', function () { }) }) it('agent should be able to access new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -874,7 +906,7 @@ describe('ACL HTTP', function () { }) }) it('agent should not be able to modify new test file', function (done) { - var options = createOptions('/acl/write-acl/default-for-new/test-file.ttl') + var options = createOptions('/acl-tls/write-acl/default-for-new/test-file.ttl') options.headers = { 'content-type': 'text/turtle' } @@ -887,8 +919,8 @@ describe('ACL HTTP', function () { }) after(function () { - rm('/acl/write-acl/default-for-new/.acl') - rm('/acl/write-acl/default-for-new/test-file.ttl') + rm('/acl-tls/write-acl/default-for-new/.acl') + rm('/acl-tls/write-acl/default-for-new/test-file.ttl') }) }) @@ -911,33 +943,33 @@ describe('ACL HTTP', function () { done() }) }) - // it("user2 should be able to make requests on behalf of user1", function(done) { - // var options = createOptions(abcdFile, 'user2') - // options.headers = { - // 'content-type': 'text/turtle', - // 'On-Behalf-Of': '<' + user1 + '>' - // } - // options.body = " ." - // request.post(options, function(error, response, body) { - // assert.equal(error, null) - // assert.equal(response.statusCode, 200) - // done() - // }) - // }) + // it("user2 should be able to make requests on behalf of user1", function(done) { + // var options = createOptions(abcdFile, 'user2') + // options.headers = { + // 'content-type': 'text/turtle', + // 'On-Behalf-Of': '<' + user1 + '>' + // } + // options.body = " ." + // request.post(options, function(error, response, body) { + // assert.equal(error, null) + // assert.equal(response.statusCode, 200) + // done() + // }) + // }) }) - describe.skip('Cleaup', function () { + describe.skip('Cleanup', function () { it('should remove all files and dirs created', function (done) { try { // must remove the ACLs in sync - fs.unlinkSync(path.join(__dirname, '/resources/' + testDir + '/dir1/dir2/abcd.ttl')) - fs.rmdirSync(path.join(__dirname, '/resources/' + testDir + '/dir1/dir2/')) - fs.rmdirSync(path.join(__dirname, '/resources/' + testDir + '/dir1/')) - fs.unlinkSync(path.join(__dirname, '/resources/' + abcFile)) - fs.unlinkSync(path.join(__dirname, '/resources/' + testDirAclFile)) - fs.unlinkSync(path.join(__dirname, '/resources/' + testDirMetaFile)) - fs.rmdirSync(path.join(__dirname, '/resources/' + testDir)) - fs.rmdirSync(path.join(__dirname, '/resources/acl/')) + fs.unlinkSync(path.join(__dirname, '../resources/' + testDir + '/dir1/dir2/abcd.ttl')) + fs.rmdirSync(path.join(__dirname, '../resources/' + testDir + '/dir1/dir2/')) + fs.rmdirSync(path.join(__dirname, '../resources/' + testDir + '/dir1/')) + fs.unlinkSync(path.join(__dirname, '../resources/' + abcFile)) + fs.unlinkSync(path.join(__dirname, '../resources/' + testDirAclFile)) + fs.unlinkSync(path.join(__dirname, '../resources/' + testDirMetaFile)) + fs.rmdirSync(path.join(__dirname, '../resources/' + testDir)) + fs.rmdirSync(path.join(__dirname, '../resources/acl-tls/')) done() } catch (e) { done(e) diff --git a/test/integration/authentication-oidc.js b/test/integration/authentication-oidc.js new file mode 100644 index 000000000..2cb86b36c --- /dev/null +++ b/test/integration/authentication-oidc.js @@ -0,0 +1,219 @@ +const Solid = require('../../index') +const path = require('path') +const supertest = require('supertest') +const expect = require('chai').expect +const nock = require('nock') +const fs = require('fs-extra') +const { UserStore } = require('oidc-auth-manager') +const UserAccount = require('../../lib/models/user-account') + +// In this test we always assume that we are Alice + +describe('Authentication API (OIDC)', () => { + let alice, aliceServer + let bob, bobServer + + let aliceServerUri = 'https://localhost:7000' + let aliceWebId = 'https://localhost:7000/profile/card#me' + let configPath = path.join(__dirname, '../../config') + let aliceDbPath = path.join(__dirname, + '../resources/accounts-scenario/alice/db') + let userStorePath = path.join(aliceDbPath, 'oidc/users') + let aliceUserStore = UserStore.from({ path: userStorePath, saltRounds: 1 }) + aliceUserStore.initCollections() + + let bobServerUri = 'https://localhost:7001' + let bobDbPath = path.join(__dirname, + '../resources/accounts-scenario/bob/db') + + const serverConfig = { + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + auth: 'oidc', + dataBrowser: false, + fileBrowser: false, + webid: true, + idp: false, + configPath + } + + const aliceRootPath = path.join(__dirname, '../resources/accounts-scenario/alice') + const alicePod = Solid.createServer( + Object.assign({ + root: aliceRootPath, + serverUri: aliceServerUri, + dbPath: aliceDbPath + }, serverConfig) + ) + const bobRootPath = path.join(__dirname, '../resources/accounts-scenario/bob') + const bobPod = Solid.createServer( + Object.assign({ + root: bobRootPath, + serverUri: bobServerUri, + dbPath: bobDbPath + }, serverConfig) + ) + + function startServer (pod, port) { + return new Promise((resolve) => { + pod.listen(port, () => { resolve() }) + }) + } + + before(() => { + return Promise.all([ + startServer(alicePod, 7000), + startServer(bobPod, 7001) + ]).then(() => { + alice = supertest(aliceServerUri) + bob = supertest(bobServerUri) + }) + }) + + after(() => { + if (aliceServer) aliceServer.close() + if (bobServer) bobServer.close() + fs.removeSync(path.join(aliceDbPath, 'oidc/users')) + fs.removeSync(path.join(aliceRootPath, 'index.html')) + fs.removeSync(path.join(aliceRootPath, 'index.html.acl')) + fs.removeSync(path.join(bobRootPath, 'index.html')) + fs.removeSync(path.join(bobRootPath, 'index.html.acl')) + }) + + describe('Provider Discovery (POST /api/auth/select-provider)', () => { + it('form should load on a get', done => { + alice.get('/api/auth/select-provider') + .expect(200) + .expect((res) => { res.text.match(/Provider Discovery/) }) + .end(done) + }) + + it('should complain if WebID URI is missing', (done) => { + alice.post('/api/auth/select-provider') + .expect(400, done) + }) + + it('should prepend https:// to webid, if necessary', (done) => { + alice.post('/api/auth/select-provider') + .type('form') + .send({ webid: 'localhost:7000' }) + .expect(302, done) + }) + + it("should return a 400 if endpoint doesn't have Link Headers", (done) => { + // Fake provider, replies with 200 and no Link headers + nock('https://amazingwebsite.tld').intercept('/', 'OPTIONS').reply(204) + + alice.post('/api/auth/select-provider') + .send('webid=https://amazingwebsite.tld/') + .expect(400) + .end(done) + }) + + it('should redirect user to discovered provider if valid uri', (done) => { + bob.post('/api/auth/select-provider') + .send('webid=' + aliceServerUri) + .expect(302) + .end((err, res) => { + let loginUri = res.header.location + expect(loginUri.startsWith(aliceServerUri + '/authorize')) + done(err) + }) + }) + }) + + describe('Login page (GET /login)', () => { + it('should load the user login form', () => { + return alice.get('/login') + .expect(200) + }) + }) + + describe('Login by Username and Password (POST /login/password)', () => { + // Logging in as alice, to alice's pod + let aliceAccount = UserAccount.from({ webId: aliceWebId }) + let alicePassword = '12345' + + beforeEach(() => { + aliceUserStore.initCollections() + + return aliceUserStore.createUser(aliceAccount, alicePassword) + .catch(console.error.bind(console)) + }) + + afterEach(() => { + fs.removeSync(path.join(aliceDbPath, 'users/users')) + }) + + it('should login and be redirected to /authorize', (done) => { + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .send({ password: alicePassword }) + .expect(302) + .expect('set-cookie', /connect.sid/) + .end((err, res) => { + let loginUri = res.header.location + expect(loginUri.startsWith(aliceServerUri + '/authorize')) + done(err) + }) + }) + + it('should throw a 400 if no username is provided', (done) => { + alice.post('/login/password') + .type('form') + .send({ password: alicePassword }) + .expect(400, done) + }) + + it('should throw a 400 if no password is provided', (done) => { + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .expect(400, done) + }) + + it('should throw a 400 if user is found but no password match', (done) => { + alice.post('/login/password') + .type('form') + .send({ username: 'alice' }) + .send({ password: 'wrongpassword' }) + .expect(400, done) + }) + }) + + describe('Login workflow', () => { + // Step 1: Alice tries to access bob.com/foo, and + // gets redirected to bob.com's Provider Discovery endpoint + it('401 Unauthorized -> redirect to provider discovery', (done) => { + bob.get('/foo') + .expect(401) + .end((err, res) => { + if (err) return done(err) + let redirectString = 'http-equiv="refresh" ' + + `content="0; url=${bobServerUri}/api/auth/select-provider` + expect(res.text).to.match(new RegExp(redirectString)) + done() + }) + }) + + // Step 2: Alice enters her WebID URI to the Provider Discovery endpoint + it('Enter webId -> redirect to provider login', (done) => { + bob.post('/api/auth/select-provider') + .send('webid=' + aliceServerUri) + .expect(302) + .end((err, res) => { + let loginUri = res.header.location + expect(loginUri.startsWith(aliceServerUri + '/authorize')) + done(err) + }) + }) + }) + + describe('Post-logout page (GET /goodbye)', () => { + it('should load the post-logout page', () => { + return alice.get('/goodbye') + .expect(200) + }) + }) +}) diff --git a/test/integration/capability-discovery.js b/test/integration/capability-discovery.js new file mode 100644 index 000000000..8f9f4dbe3 --- /dev/null +++ b/test/integration/capability-discovery.js @@ -0,0 +1,106 @@ +const Solid = require('../../index') +const path = require('path') +const fs = require('fs-extra') +const supertest = require('supertest') +const expect = require('chai').expect +// In this test we always assume that we are Alice + +describe('API', () => { + let alice, aliceServer + + let aliceServerUri = 'https://localhost:5000' + let configPath = path.join(__dirname, '../../config') + let aliceDbPath = path.join(__dirname, + '../resources/accounts-scenario/alice/db') + let aliceRootPath = path.join(__dirname, '../resources/accounts-scenario/alice') + + const serverConfig = { + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + auth: 'oidc', + dataBrowser: false, + fileBrowser: false, + webid: true, + idp: false, + configPath + } + + const alicePod = Solid.createServer( + Object.assign({ + root: aliceRootPath, + serverUri: aliceServerUri, + dbPath: aliceDbPath + }, serverConfig) + ) + + function startServer (pod, port) { + return new Promise((resolve) => { + pod.listen(port, () => { resolve() }) + }) + } + + before(() => { + return Promise.all([ + startServer(alicePod, 5000) + ]).then(() => { + alice = supertest(aliceServerUri) + }) + }) + + after(() => { + if (aliceServer) aliceServer.close() + fs.removeSync(path.join(aliceRootPath, 'index.html')) + fs.removeSync(path.join(aliceRootPath, 'index.html.acl')) + }) + + describe('Capability Discovery', () => { + describe('GET Service Capability document', () => { + it('should exist', (done) => { + alice.get('/.well-known/solid') + .expect(200, done) + }) + it('should be a json file by default', (done) => { + alice.get('/.well-known/solid') + .expect('content-type', /application\/json/) + .expect(200, done) + }) + it('includes a root element', (done) => { + alice.get('/.well-known/solid') + .end(function (err, req) { + expect(req.body.root).to.exist + return done(err) + }) + }) + it('includes an apps config section', (done) => { + const config = { + apps: { + 'signin': '/signin/', + 'signup': '/signup/' + }, + webid: false + } + const solid = Solid(config) + let server = supertest(solid) + server.get('/.well-known/solid') + .end(function (err, req) { + expect(req.body.apps).to.exist + return done(err) + }) + }) + }) + + describe('OPTIONS API', () => { + it('should return the service Link header', (done) => { + alice.options('/') + .expect('Link', /<.*\.well-known\/solid>; rel="service"/) + .expect(204, done) + }) + + it('should return the oidc.provider Link header', (done) => { + alice.options('/') + .expect('Link', /; rel="oidc.provider"/) + .expect(204, done) + }) + }) + }) +}) diff --git a/test/integration/errors-oidc.js b/test/integration/errors-oidc.js new file mode 100644 index 000000000..2ee70de84 --- /dev/null +++ b/test/integration/errors-oidc.js @@ -0,0 +1,97 @@ +const supertest = require('supertest') +const ldnode = require('../../index') +const path = require('path') +const fs = require('fs-extra') +const expect = require('chai').expect + +describe('OIDC error handling', function () { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + + const serverUri = 'https://localhost:3457' + var ldpHttpsServer + const rootPath = path.join(__dirname, '../resources/accounts/errortests') + const dbPath = path.join(__dirname, '../resources/accounts/db') + + const ldp = ldnode.createServer({ + root: rootPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + auth: 'oidc', + webid: true, + idp: false, + strictOrigin: true, + dbPath, + serverUri + }) + + before(function (done) { + ldpHttpsServer = ldp.listen(3457, done) + }) + + after(function () { + if (ldpHttpsServer) ldpHttpsServer.close() + fs.removeSync(path.join(rootPath, 'index.html')) + fs.removeSync(path.join(rootPath, 'index.html.acl')) + }) + + const server = supertest(serverUri) + + describe('Unauthenticated requests to protected resources', () => { + describe('accepting text/html', () => { + it('should return 401 Unauthorized with www-auth header', () => { + return server.get('/profile/') + .set('Accept', 'text/html') + .expect('WWW-Authenticate', 'Bearer realm="https://localhost:3457", scope="openid"') + .expect(401) + }) + + it('should return an html redirect body', () => { + return server.get('/profile/') + .set('Accept', 'text/html') + .expect('Content-Type', 'text/html; charset=utf-8') + .then(res => { + expect(res.text).to.match(/ { + describe('with an empty bearer token', () => { + it('should return a 400 error', () => { + return server.get('/profile/') + .set('Authorization', 'Bearer ') + .expect(400) + }) + }) + + describe('with an invalid bearer token', () => { + it('should return a 401 error', () => { + return server.get('/profile/') + .set('Authorization', 'Bearer abcd123') + .expect('WWW-Authenticate', 'Bearer realm="https://localhost:3457", scope="openid", error="invalid_token", error_description="Access token is not a JWT"') + .expect(401) + }) + }) + + describe('with an expired bearer token', () => { + const expiredToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImxOWk9CLURQRTFrIn0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDozNDU3Iiwic3ViIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6MzQ1Ny9wcm9maWxlL2NhcmQjbWUiLCJhdWQiOiJodHRwczovL2xvY2FsaG9zdDozNDU3IiwiZXhwIjoxNDk2MjM5ODY1LCJpYXQiOjE0OTYyMzk4NjUsImp0aSI6IjliN2MwNGQyNDY3MjQ1ZWEiLCJub25jZSI6IklXaUpMVFNZUmktVklSSlhjejVGdU9CQTFZR1lZNjFnRGRlX2JnTEVPMDAiLCJhdF9oYXNoIjoiRFpES3I0RU1xTGE1Q0x1elV1WW9pdyJ9.uBTLy_wG5rr4kxM0hjXwIC-NwGYrGiiiY9IdOk5hEjLj2ECc767RU7iZ5vZa0pSrGy0V2Y3BiZ7lnYIA7N4YUAuS077g_4zavoFWyu9xeq6h70R8yfgFUNPo91PGpODC9hgiNbEv2dPBzTYYHqf7D6_-3HGnnDwiX7TjWLTkPLRvPLTcsCUl7G7y-EedjcVRk3Jyv8TNSoBMeTwOR3ewuzNostmCjUuLsr73YpVid6HE55BBqgSCDCNtS-I7nYmO_lRqIWJCydjdStSMJgxzSpASvoeCJ_lwZF6FXmZOQNNhmstw69fU85J1_QsS78cRa76-SnJJp6JCWHFBUAolPQ' + + it('should return a 401 error', () => { + return server.get('/profile/') + .set('Authorization', 'Bearer ' + expiredToken) + .expect('WWW-Authenticate', 'Bearer realm="https://localhost:3457", scope="openid", error="invalid_token", error_description="Access token is expired."') + .expect(401) + }) + }) + }) +}) diff --git a/test/errors.js b/test/integration/errors.js similarity index 81% rename from test/errors.js rename to test/integration/errors.js index d73059ca2..eb67ccd5c 100644 --- a/test/errors.js +++ b/test/integration/errors.js @@ -5,22 +5,24 @@ var path = require('path') // var rm = require('./test-utils').rm // var write = require('./test-utils').write // var cp = require('./test-utils').cp -var read = require('./test-utils').read +var read = require('./../test-utils').read -var ldnode = require('../index') +var ldnode = require('../../index') describe('Error pages', function () { // LDP with error pages var errorLdp = ldnode({ - root: path.join(__dirname, '/resources'), - errorPages: path.join(__dirname, '/resources/errorPages') + root: path.join(__dirname, '../resources'), + errorPages: path.join(__dirname, '../resources/errorPages'), + webid: false }) var errorServer = supertest(errorLdp) // LDP with no error pages var noErrorLdp = ldnode({ - root: path.join(__dirname, '/resources'), - noErrorPages: true + root: path.join(__dirname, '../resources'), + noErrorPages: true, + webid: false }) var noErrorServer = supertest(noErrorLdp) diff --git a/test/formats.js b/test/integration/formats.js similarity index 97% rename from test/formats.js rename to test/integration/formats.js index e4b216d2a..3c776599d 100644 --- a/test/formats.js +++ b/test/integration/formats.js @@ -1,10 +1,11 @@ var supertest = require('supertest') -var ldnode = require('../index') +var ldnode = require('../../index') var path = require('path') describe('formats', function () { var ldp = ldnode.createServer({ - root: path.join(__dirname, '/resources') + root: path.join(__dirname, '../resources'), + webid: false }) var server = supertest(ldp) diff --git a/test/http-copy.js b/test/integration/http-copy.js similarity index 78% rename from test/http-copy.js rename to test/integration/http-copy.js index 3f9368bc7..7a2684c6e 100644 --- a/test/http-copy.js +++ b/test/integration/http-copy.js @@ -3,9 +3,9 @@ var fs = require('fs') var request = require('request') var path = require('path') // Helper functions for the FS -var rm = require('./test-utils').rm +var rm = require('./../test-utils').rm -var solidServer = require('../index') +var solidServer = require('../../index') describe('HTTP COPY API', function () { this.timeout(10000) @@ -15,9 +15,10 @@ describe('HTTP COPY API', function () { var ldpHttpsServer var ldp = solidServer.createServer({ - root: path.join(__dirname, 'resources/accounts/localhost/'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem') + root: path.join(__dirname, '../resources/accounts/localhost/'), + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), + webid: false }) before(function (done) { @@ -34,12 +35,12 @@ describe('HTTP COPY API', function () { var userCredentials = { user1: { - cert: fs.readFileSync(path.join(__dirname, '/keys/user1-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user1-key.pem')) + cert: fs.readFileSync(path.join(__dirname, '../keys/user1-cert.pem')), + key: fs.readFileSync(path.join(__dirname, '../keys/user1-key.pem')) }, user2: { - cert: fs.readFileSync(path.join(__dirname, '/keys/user2-cert.pem')), - key: fs.readFileSync(path.join(__dirname, '/keys/user2-key.pem')) + cert: fs.readFileSync(path.join(__dirname, '../keys/user2-cert.pem')), + key: fs.readFileSync(path.join(__dirname, '../keys/user2-key.pem')) } } @@ -65,7 +66,7 @@ describe('HTTP COPY API', function () { assert.equal(error, null) assert.equal(response.statusCode, 201) assert.equal(response.headers[ 'location' ], copyTo) - let destinationPath = path.join(__dirname, 'resources/accounts/localhost', copyTo) + let destinationPath = path.join(__dirname, '../resources/accounts/localhost', copyTo) assert.ok(fs.existsSync(destinationPath), 'Resource created via COPY should exist') done() diff --git a/test/http.js b/test/integration/http.js similarity index 86% rename from test/http.js rename to test/integration/http.js index 8306e11af..b7c4645f3 100644 --- a/test/http.js +++ b/test/integration/http.js @@ -1,8 +1,8 @@ var supertest = require('supertest') var fs = require('fs') var li = require('li') -var ldnode = require('../index') -var rm = require('./test-utils').rm +var ldnode = require('../../index') +var rm = require('./../test-utils').rm var path = require('path') var suffixAcl = '.acl' @@ -10,7 +10,11 @@ var suffixMeta = '.meta' var ldpServer = ldnode.createServer({ live: true, dataBrowserPath: 'default', - root: path.join(__dirname, '/resources') + root: path.join(__dirname, '../resources'), + auth: 'oidc', + webid: false, + suffixAcl, + suffixMeta }) var server = supertest(ldpServer) var assert = require('chai').assert @@ -54,7 +58,7 @@ function createTestResource (resourceName) { describe('HTTP APIs', function () { var emptyResponse = function (res) { - if (res.text.length !== 0) { + if (res.text) { console.log('Not empty response') } } @@ -103,7 +107,7 @@ describe('HTTP APIs', function () { .expect('Access-Control-Allow-Origin', 'http://example.com') .expect('Access-Control-Allow-Credentials', 'true') .expect('Access-Control-Allow-Methods', 'OPTIONS,HEAD,GET,PATCH,POST,PUT,DELETE') - .expect('Access-Control-Expose-Headers', 'User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, Content-Length') + .expect('Access-Control-Expose-Headers', 'Authorization, User, Location, Link, Vary, Last-Modified, ETag, Accept-Patch, Accept-Post, Updates-Via, Allow, Content-Length, WWW-Authenticate') .expect(204, done) }) @@ -193,7 +197,7 @@ describe('HTTP APIs', function () { } var size = fs.statSync(path.join(__dirname, - '/resources/sampleContainer/solid.png')).size + '../resources/sampleContainer/solid.png')).size if (res.body.length !== size) { return done(new Error('files are not of the same size')) } @@ -260,7 +264,7 @@ describe('HTTP APIs', function () { .expect(200, done) }) it('should redirect to file browser if container was requested as text/html', function (done) { - server.get('/') + server.get('/sampleContainer2/') .set('Accept', 'text/html') .expect('content-type', /text\/html/) .expect(303, done) @@ -374,7 +378,7 @@ describe('HTTP APIs', function () { describe('PUT API', function () { var putRequestBody = fs.readFileSync(path.join(__dirname, - '/resources/sampleContainer/put1.ttl'), { + '../resources/sampleContainer/put1.ttl'), { 'encoding': 'utf8' }) it('should create new resource', function (done) { @@ -391,15 +395,71 @@ describe('HTTP APIs', function () { .expect(hasHeader('acl', 'baz.ttl' + suffixAcl)) .expect(201, done) }) - it('should return 409 code when trying to put to a container', - function (done) { - server.put('/') - .expect(409, done) - } - ) - // Cleanup - after(function () { - rm('/foo/') + + describe('PUT and containers', () => { + const containerMeta = fs.readFileSync(path.join(__dirname, + '../resources/sampleContainer/post2.ttl'), + { 'encoding': 'utf8' }) + + after(() => { + rm('/foo/') + }) + + it('should create a container (implicit from url)', () => { + return server.put('/foo/two/') + .expect(201) + .then(() => { + let stats = fs.statSync(path.join(__dirname, '../resources/foo/two/')) + + assert(stats.isDirectory(), 'Cannot read container just created') + }) + }) + + it('should create a container (explicit from link header)', () => { + return server.put('/foo/three') + .set('link', '; rel="type"') + .expect(201) + .then(() => { + let stats = fs.statSync(path.join(__dirname, '../resources/foo/three/')) + + assert(stats.isDirectory(), 'Cannot read container just created') + }) + }) + + it('should write the request body to the container .meta', () => { + return server.put('/foo/four/') + .set('content-type', 'text/turtle') + .set('link', '; rel="type"') + .send(containerMeta) + .expect(201) + .then(() => { + let metaFilePath = path.join(__dirname, '../resources/foo/four/' + suffixMeta) + let meta = fs.readFileSync(metaFilePath, 'utf8') + + assert.equal(meta, containerMeta) + }) + }) + + it('should update existing container .meta', () => { + let newMeta = '<> dcterms:title "Home loans".' + + return server.put('/foo/five/') + .set('content-type', 'text/turtle') + .send(containerMeta) + .expect(201) + .then(() => { + return server.put('/foo/five/') + .set('content-type', 'text/turtle') + .send(newMeta) + .expect(204) + }) + .then(() => { + let metaFilePath = path.join(__dirname, '../resources/foo/five/' + suffixMeta) + let meta = fs.readFileSync(metaFilePath, 'utf8') + + assert.equal(meta, newMeta) + }) + }) }) }) @@ -459,11 +519,11 @@ describe('HTTP APIs', function () { }) var postRequest1Body = fs.readFileSync(path.join(__dirname, - '/resources/sampleContainer/put1.ttl'), { + '../resources/sampleContainer/put1.ttl'), { 'encoding': 'utf8' }) var postRequest2Body = fs.readFileSync(path.join(__dirname, - '/resources/sampleContainer/post2.ttl'), { + '../resources/sampleContainer/post2.ttl'), { 'encoding': 'utf8' }) it('should create new resource', function (done) { @@ -533,7 +593,7 @@ describe('HTTP APIs', function () { .expect(201) .end(function (err) { if (err) return done(err) - var stats = fs.statSync(path.join(__dirname, '/resources/post-tests/loans/')) + var stats = fs.statSync(path.join(__dirname, '../resources/post-tests/loans/')) if (!stats.isDirectory()) { return done(new Error('Cannot read container just created')) } @@ -559,7 +619,7 @@ describe('HTTP APIs', function () { try { assert.equal(res.headers.location, expectedDirName, 'Uri container names should be encoded') - let createdDir = fs.statSync(path.join(__dirname, 'resources', expectedDirName)) + let createdDir = fs.statSync(path.join(__dirname, '../resources', expectedDirName)) assert(createdDir.isDirectory(), 'Container should have been created') } catch (err) { return done(err) @@ -612,19 +672,19 @@ describe('HTTP APIs', function () { it('should create as many files as the ones passed in multipart', function (done) { server.post('/sampleContainer/') - .attach('timbl', path.join(__dirname, '/resources/timbl.jpg')) - .attach('nicola', path.join(__dirname, '/resources/nicola.jpg')) + .attach('timbl', path.join(__dirname, '../resources/timbl.jpg')) + .attach('nicola', path.join(__dirname, '../resources/nicola.jpg')) .expect(200) .end(function (err) { if (err) return done(err) var sizeNicola = fs.statSync(path.join(__dirname, - '/resources/nicola.jpg')).size - var sizeTim = fs.statSync(path.join(__dirname, '/resources/timbl.jpg')).size + '../resources/nicola.jpg')).size + var sizeTim = fs.statSync(path.join(__dirname, '../resources/timbl.jpg')).size var sizeNicolaLocal = fs.statSync(path.join(__dirname, - '/resources/sampleContainer/nicola.jpg')).size + '../resources/sampleContainer/nicola.jpg')).size var sizeTimLocal = fs.statSync(path.join(__dirname, - '/resources/sampleContainer/timbl.jpg')).size + '../resources/sampleContainer/timbl.jpg')).size if (sizeNicola === sizeNicolaLocal && sizeTim === sizeTimLocal) { return done() diff --git a/test/ldp.js b/test/integration/ldp.js similarity index 75% rename from test/ldp.js rename to test/integration/ldp.js index 34dd6c661..52a50b668 100644 --- a/test/ldp.js +++ b/test/integration/ldp.js @@ -1,20 +1,24 @@ var assert = require('chai').assert var $rdf = require('rdflib') var ns = require('solid-namespace')($rdf) -var LDP = require('../lib/ldp') +var LDP = require('../../lib/ldp') var path = require('path') -var stringToStream = require('../lib/utils').stringToStream +var stringToStream = require('../../lib/utils').stringToStream // Helper functions for the FS -var rm = require('./test-utils').rm -var write = require('./test-utils').write +var rm = require('./../test-utils').rm +var write = require('./../test-utils').write // var cp = require('./test-utils').cp -var read = require('./test-utils').read +var read = require('./../test-utils').read var fs = require('fs') +const suffixMeta = '.meta' + describe('LDP', function () { var ldp = new LDP({ - root: __dirname + suffixMeta, + root: path.join(__dirname, '..'), + webid: false }) describe('readFile', function () { @@ -28,7 +32,7 @@ describe('LDP', function () { it('return file if file exists', function (done) { // file can be empty as well write('hello world', 'fileExists.txt') - ldp.readFile(path.join(__dirname, '/resources/fileExists.txt'), function (err, file) { + ldp.readFile(path.join(__dirname, '../resources/fileExists.txt'), function (err, file) { rm('fileExists.txt') assert.notOk(err) assert.equal(file, 'hello world') @@ -48,7 +52,7 @@ describe('LDP', function () { it('should return content if metaFile exists', function (done) { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '/resources/'), function (err, metaFile) { + ldp.readContainerMeta(path.join(__dirname, '../resources/'), function (err, metaFile) { rm('.meta') assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') @@ -59,7 +63,7 @@ describe('LDP', function () { it('should work also if trailing `/` is not passed', function (done) { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '/resources'), function (err, metaFile) { + ldp.readContainerMeta(path.join(__dirname, '../resources'), function (err, metaFile) { rm('.meta') assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') @@ -92,6 +96,10 @@ describe('LDP', function () { }) describe('putGraph', () => { + after(() => { + rm('sampleContainer/example1-copy.ttl') + }) + it('should serialize and write a graph to a file', () => { let originalResource = '/resources/sampleContainer/example1.ttl' let newResource = '/resources/sampleContainer/example1-copy.ttl' @@ -107,13 +115,19 @@ describe('LDP', function () { let written = read('sampleContainer/example1-copy.ttl') assert.ok(written) }) - // cleanup - .then(() => { rm('sampleContainer/example1-copy.ttl') }) - .catch(() => { rm('sampleContainer/example1-copy.ttl') }) }) }) describe('put', function () { + before(() => { + rm('testPut.txt') + }) + + after(() => { + rm('new-container/') + rm('new-container2/') + }) + it('should write a file in an existing dir', function (done) { var stream = stringToStream('hello world') ldp.put('localhost', '/resources/testPut.txt', stream, function (err) { @@ -125,13 +139,47 @@ describe('LDP', function () { }) }) - it('should fail if a trailing `/` is passed', function (done) { - var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/', stream, function (err) { - assert.equal(err.status, 409) + it('should create a new container', done => { + const containerMeta = '<> dcterms:title "Home loans".' + const stream = stringToStream(containerMeta) + + ldp.put('localhost', '/resources/new-container/', stream, (err, status) => { + if (err) { return done(err) } + + assert.equal(status, 201) + + let written = read('new-container/' + suffixMeta) + assert.equal(written, containerMeta) + done() }) }) + + it('should update existing container meta', done => { + const containerMeta = '<> dcterms:title "Home loans".' + const newMeta = '<> dcterms:title "Car loans".' + + let stream = stringToStream(containerMeta) + + ldp.put('localhost', '/resources/new-container2/', stream, (err, status) => { + if (err) { return done(err) } + + assert.equal(status, 201) + + stream = stringToStream(newMeta) + + ldp.put('localhost', '/resources/new-container2/', stream, (err, status) => { + if (err) { return done(err) } + + assert.equal(status, 204) + + let written = read('new-container2/' + suffixMeta) + assert.equal(written, newMeta) + + done() + }) + }) + }) }) describe('delete', function () { @@ -162,7 +210,7 @@ describe('LDP', function () { ' dcterms:title "This is a magic type" ;' + ' o:limit 500000.00 .', 'sampleContainer/magicType.ttl') - ldp.listContainer(path.join(__dirname, '/resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { if (err) done(err) var graph = $rdf.graph() $rdf.parse( @@ -205,7 +253,7 @@ describe('LDP', function () { ' dcterms:title "This is a container" ;' + ' o:limit 500000.00 .', 'sampleContainer/basicContainerFile.ttl') - ldp.listContainer(path.join(__dirname, '/resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { if (err) done(err) var graph = $rdf.graph() $rdf.parse( @@ -245,9 +293,9 @@ describe('LDP', function () { }) it('should ldp:contains the same amount of files in dir', function (done) { - ldp.listContainer(path.join(__dirname, '/resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { if (err) done(err) - fs.readdir(path.join(__dirname, '/resources/sampleContainer/'), function (err, files) { + fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, files) { var graph = $rdf.graph() $rdf.parse( data, diff --git a/test/integration/oidc-manager.js b/test/integration/oidc-manager.js new file mode 100644 index 000000000..e57ad2375 --- /dev/null +++ b/test/integration/oidc-manager.js @@ -0,0 +1,39 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const path = require('path') +const fs = require('fs-extra') + +const OidcManager = require('../../lib/models/oidc-manager') +const SolidHost = require('../../lib/models/solid-host') + +const dbPath = path.join(__dirname, '../resources/.db') + +describe('OidcManager', () => { + beforeEach(() => { + fs.removeSync(dbPath) + }) + + describe('fromServerConfig()', () => { + it('should result in an initialized oidc object', () => { + let serverUri = 'https://localhost:8443' + let host = SolidHost.from({ serverUri }) + + let saltRounds = 5 + let argv = { + host, + dbPath, + saltRounds + } + + let oidc = OidcManager.fromServerConfig(argv) + + expect(oidc.rs.defaults.query).to.be.true + expect(oidc.clients.store.backend.path.endsWith('db/oidc/rp/clients')) + expect(oidc.provider.issuer).to.equal(serverUri) + expect(oidc.users.backend.path.endsWith('db/oidc/users')) + expect(oidc.users.saltRounds).to.equal(saltRounds) + }) + }) +}) diff --git a/test/params.js b/test/integration/params.js similarity index 72% rename from test/params.js rename to test/integration/params.js index 481743ece..aeedd8cb3 100644 --- a/test/params.js +++ b/test/integration/params.js @@ -1,19 +1,20 @@ var assert = require('chai').assert var supertest = require('supertest') var path = require('path') +const fs = require('fs-extra') // Helper functions for the FS -var rm = require('./test-utils').rm -var write = require('./test-utils').write +var rm = require('../test-utils').rm +var write = require('../test-utils').write // var cp = require('./test-utils').cp -var read = require('./test-utils').read +var read = require('../test-utils').read -var ldnode = require('../index') +var ldnode = require('../../index') describe('LDNODE params', function () { describe('suffixMeta', function () { describe('not passed', function () { it('should fallback on .meta', function () { - var ldp = ldnode() + var ldp = ldnode({ webid: false }) assert.equal(ldp.locals.ldp.suffixMeta, '.meta') }) }) @@ -22,7 +23,7 @@ describe('LDNODE params', function () { describe('suffixAcl', function () { describe('not passed', function () { it('should fallback on .acl', function () { - var ldp = ldnode() + var ldp = ldnode({ webid: false }) assert.equal(ldp.locals.ldp.suffixAcl, '.acl') }) }) @@ -30,7 +31,7 @@ describe('LDNODE params', function () { describe('root', function () { describe('not passed', function () { - var ldp = ldnode() + var ldp = ldnode({ webid: false }) var server = supertest(ldp) it('should fallback on current working directory', function () { @@ -55,7 +56,7 @@ describe('LDNODE params', function () { }) describe('passed', function () { - var ldp = ldnode({root: './test/resources/'}) + var ldp = ldnode({root: './test/resources/', webid: false}) var server = supertest(ldp) it('should fallback on current working directory', function () { @@ -81,9 +82,11 @@ describe('LDNODE params', function () { }) describe('ui-path', function () { + let rootPath = './test/resources/' var ldp = ldnode({ - root: './test/resources/', - apiApps: path.join(__dirname, 'resources/sampleContainer') + root: rootPath, + apiApps: path.join(__dirname, '../resources/sampleContainer'), + webid: false }) var server = supertest(ldp) @@ -96,24 +99,37 @@ describe('LDNODE params', function () { describe('forcedUser', function () { var ldpHttpsServer + + const port = 7777 + const serverUri = `https://localhost:7777` + const rootPath = path.join(__dirname, '../resources/accounts-acl') + const dbPath = path.join(rootPath, 'db') + const configPath = path.join(rootPath, 'config') + var ldp = ldnode.createServer({ forceUser: 'https://fakeaccount.com/profile#me', - root: path.join(__dirname, '/resources/acl/fake-account'), - sslKey: path.join(__dirname, '/keys/key.pem'), - sslCert: path.join(__dirname, '/keys/cert.pem'), + dbPath, + configPath, + serverUri, + port, + root: rootPath, + sslKey: path.join(__dirname, '../keys/key.pem'), + sslCert: path.join(__dirname, '../keys/cert.pem'), webid: true, host: 'localhost:3457' }) before(function (done) { - ldpHttpsServer = ldp.listen(3459, done) + ldpHttpsServer = ldp.listen(port, done) }) after(function () { if (ldpHttpsServer) ldpHttpsServer.close() + fs.removeSync(path.join(rootPath, 'index.html')) + fs.removeSync(path.join(rootPath, 'index.html.acl')) }) - var server = supertest('https://localhost:3459') + var server = supertest(serverUri) it('should find resource in correct path', function (done) { server.get('/hello.html') diff --git a/test/patch-2.js b/test/integration/patch-2.js similarity index 96% rename from test/patch-2.js rename to test/integration/patch-2.js index 0b8bfda80..e213eca7b 100644 --- a/test/patch-2.js +++ b/test/integration/patch-2.js @@ -1,19 +1,20 @@ -var ldnode = require('../') +var ldnode = require('../../index') var supertest = require('supertest') var assert = require('chai').assert var path = require('path') // Helper functions for the FS -var rm = require('./test-utils').rm -var write = require('./test-utils').write +var rm = require('../test-utils').rm +var write = require('../test-utils').write // var cp = require('./test-utils').cp -var read = require('./test-utils').read +var read = require('../test-utils').read describe('PATCH', function () { // Starting LDP var ldp = ldnode({ - root: path.join(__dirname, '/resources/sampleContainer'), - mount: '/test' + root: path.join(__dirname, '../resources/sampleContainer'), + mount: '/test', + webid: false }) var server = supertest(ldp) diff --git a/test/patch.js b/test/integration/patch.js similarity index 95% rename from test/patch.js rename to test/integration/patch.js index 485161168..0c337eb87 100644 --- a/test/patch.js +++ b/test/integration/patch.js @@ -1,19 +1,20 @@ -var ldnode = require('../') +var ldnode = require('../../index') var supertest = require('supertest') var assert = require('chai').assert var path = require('path') // Helper functions for the FS -var rm = require('./test-utils').rm -var write = require('./test-utils').write +var rm = require('../test-utils').rm +var write = require('../test-utils').write // var cp = require('./test-utils').cp -var read = require('./test-utils').read +var read = require('../test-utils').read describe('PATCH', function () { // Starting LDP var ldp = ldnode({ - root: path.join(__dirname, '/resources/sampleContainer'), - mount: '/test' + root: path.join(__dirname, '../resources/sampleContainer'), + mount: '/test', + webid: false }) var server = supertest(ldp) diff --git a/test/proxy.js b/test/integration/proxy.js similarity index 96% rename from test/proxy.js rename to test/integration/proxy.js index 92289e55d..82756b4c6 100644 --- a/test/proxy.js +++ b/test/integration/proxy.js @@ -4,12 +4,13 @@ var path = require('path') var nock = require('nock') var async = require('async') -var ldnode = require('../index') +var ldnode = require('../../index') describe('proxy', () => { var ldp = ldnode({ - root: path.join(__dirname, '/resources'), - proxy: '/proxy' + root: path.join(__dirname, '../resources'), + proxy: '/proxy', + webid: false }) var server = supertest(ldp) diff --git a/test/resources/accounts-acl/config/templates/emails/welcome.js b/test/resources/accounts-acl/config/templates/emails/welcome.js new file mode 100644 index 000000000..bce554462 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/emails/welcome.js @@ -0,0 +1,39 @@ +'use strict' + +/** + * Returns a partial Email object (minus the `to` and `from` properties), + * suitable for sending with Nodemailer. + * + * Used to send a Welcome email after a new user account has been created. + * + * @param data {Object} + * + * @param data.webid {string} + * + * @return {Object} + */ +function render (data) { + return { + subject: 'Welcome to Solid', + + /** + * Text version of the Welcome email + */ + text: `Welcome to Solid! + +Your account has been created. + +Your Web Id: ${data.webid}`, + + /** + * HTML version of the Welcome email + */ + html: `

Welcome to Solid!

+ +

Your account has been created.

+ +

Your Web Id: ${data.webid}

` + } +} + +module.exports.render = render diff --git a/test/resources/accounts-acl/config/templates/new-account/.acl b/test/resources/accounts-acl/config/templates/new-account/.acl new file mode 100644 index 000000000..35258e69f --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/.acl @@ -0,0 +1,23 @@ +# Root ACL resource for the user account +@prefix acl: . + +<#owner> + a acl:Authorization; + + acl:agent <{{webId}}> ; + + # Optional owner email, to be used for account recovery: + {{#if email}}acl:agent ;{{/if}} + + # Set the access to the root storage folder itself + acl:accessTo ; + + # All resources will inherit this authorization, by default + acl:defaultForNew ; + + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. + +# Data is private by default; no other agents get access unless specifically +# authorized in other .acls diff --git a/test/resources/accounts-acl/config/templates/new-account/.meta b/test/resources/accounts-acl/config/templates/new-account/.meta new file mode 100644 index 000000000..591051f43 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/.meta @@ -0,0 +1,5 @@ +# Root Meta resource for the user account +# Used to discover the account's WebID URI, given the account URI +<{{webId}}> + + . diff --git a/test/resources/accounts-acl/config/templates/new-account/.meta.acl b/test/resources/accounts-acl/config/templates/new-account/.meta.acl new file mode 100644 index 000000000..c297ce822 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/.meta.acl @@ -0,0 +1,25 @@ +# ACL resource for the Root Meta +# Should be public-readable (since the root meta is used for WebID discovery) + +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo ; + + acl:mode + acl:Read, acl:Write, acl:Control. + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo ; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/templates/new-account/favicon.ico b/test/resources/accounts-acl/config/templates/new-account/favicon.ico new file mode 100644 index 000000000..764acb205 Binary files /dev/null and b/test/resources/accounts-acl/config/templates/new-account/favicon.ico differ diff --git a/test/resources/accounts-acl/config/templates/new-account/favicon.ico.acl b/test/resources/accounts-acl/config/templates/new-account/favicon.ico.acl new file mode 100644 index 000000000..01e11d075 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/favicon.ico.acl @@ -0,0 +1,26 @@ +# ACL for the default favicon.ico resource +# Individual users will be able to override it as they wish +# Public-readable + +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo ; + + acl:mode + acl:Read, acl:Write, acl:Control. + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo ; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/templates/new-account/inbox/.acl b/test/resources/accounts-acl/config/templates/new-account/inbox/.acl new file mode 100644 index 000000000..c10554678 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/inbox/.acl @@ -0,0 +1,26 @@ +# ACL resource for the profile Inbox + +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo <./>; + acl:defaultForNew <./>; + + acl:mode + acl:Read, acl:Write, acl:Control. + +# Public-appendable but NOT public-readable +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./>; + + acl:mode acl:Append. diff --git a/test/resources/accounts-acl/config/templates/new-account/index.html b/test/resources/accounts-acl/config/templates/new-account/index.html new file mode 100644 index 000000000..6c5abd03c --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/index.html @@ -0,0 +1,28 @@ + + + + + + Solid User Profile + + + +
+

Solid User Profile

+
+
+
+
+

+ Welcome to your Solid user profile. +

+

+ Your Web ID is:
+ + {{webId}} +

+
+
+
+ + diff --git a/test/resources/accounts-acl/config/templates/new-account/index.html.acl b/test/resources/accounts-acl/config/templates/new-account/index.html.acl new file mode 100644 index 000000000..47c7640a2 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/index.html.acl @@ -0,0 +1,22 @@ +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo ; + + acl:mode + acl:Read, acl:Write, acl:Control. + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./index.html>; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/templates/new-account/profile/card b/test/resources/accounts-acl/config/templates/new-account/profile/card new file mode 100644 index 000000000..b9e138ccf --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/profile/card @@ -0,0 +1,26 @@ +@prefix solid: . +@prefix foaf: . +@prefix pim: . +@prefix schema: . +@prefix ldp: . + +<> + a foaf:PersonalProfileDocument ; + foaf:maker <#me> ; + foaf:primaryTopic <#me> . + +<#me> + a foaf:Person ; + a schema:Person ; + + foaf:name "{{name}}" ; + + solid:account ; # link to the account uri + pim:storage ; # root storage + + solid:inbox ; + ldp:inbox ; + + pim:preferencesFile ; # private settings/preferences + solid:publicTypeIndex ; + solid:privateTypeIndex . diff --git a/test/resources/accounts-acl/config/templates/new-account/profile/card.acl b/test/resources/accounts-acl/config/templates/new-account/profile/card.acl new file mode 100644 index 000000000..335aa13da --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/profile/card.acl @@ -0,0 +1,25 @@ +# ACL for the WebID Profile document + +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo <./card>; + + acl:mode + acl:Read, acl:Write, acl:Control. + +# Public-readable +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./card>; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/.acl b/test/resources/accounts-acl/config/templates/new-account/settings/.acl new file mode 100644 index 000000000..bc871390f --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/settings/.acl @@ -0,0 +1,20 @@ +# ACL resource for the /settings/ container +@prefix acl: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + # Set the access to the root storage folder itself + acl:accessTo <./>; + + # All settings resources will be private, by default, unless overridden + acl:defaultForNew <./>; + + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. + +# Private, no public access modes diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/prefs.ttl b/test/resources/accounts-acl/config/templates/new-account/settings/prefs.ttl new file mode 100644 index 000000000..8b5e8d3bb --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/settings/prefs.ttl @@ -0,0 +1,9 @@ +@prefix dct: . +@prefix pim: . + +<> + a pim:ConfigurationFile; + + dct:title "Preferences file" . + +{{#if email}}<{{webId}}> foaf:mbox .{{/if}} diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl b/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl new file mode 100644 index 000000000..607f6321d --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/settings/privateTypeIndex.ttl @@ -0,0 +1,4 @@ +@prefix solid: . +<> + a solid:TypeIndex ; + a solid:UnlistedDocument. diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl new file mode 100644 index 000000000..e270db393 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl @@ -0,0 +1,4 @@ +@prefix solid: . +<> + a solid:TypeIndex ; + a solid:ListedDocument. diff --git a/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl.acl b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl.acl new file mode 100644 index 000000000..6a1901462 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/new-account/settings/publicTypeIndex.ttl.acl @@ -0,0 +1,25 @@ +# ACL resource for the Public Type Index + +@prefix acl: . +@prefix foaf: . + +<#owner> + a acl:Authorization; + + acl:agent + <{{webId}}>; + + acl:accessTo <./publicTypeIndex.ttl>; + + acl:mode + acl:Read, acl:Write, acl:Control. + +# Public-readable +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./publicTypeIndex.ttl>; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/templates/server/index.html b/test/resources/accounts-acl/config/templates/server/index.html new file mode 100644 index 000000000..6101fdcb7 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/server/index.html @@ -0,0 +1,35 @@ + + + + + + Welcome to Solid + + + +
+

Welcome to Solid

+
+
+
+
+

+ If you have not already done so, please create an account. +

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + diff --git a/test/resources/accounts-acl/config/templates/server/index.html.acl b/test/resources/accounts-acl/config/templates/server/index.html.acl new file mode 100644 index 000000000..de9032975 --- /dev/null +++ b/test/resources/accounts-acl/config/templates/server/index.html.acl @@ -0,0 +1,11 @@ +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./index.html>; + + acl:mode acl:Read. diff --git a/test/resources/accounts-acl/config/views/account/register.hbs b/test/resources/accounts-acl/config/views/account/register.hbs new file mode 100644 index 000000000..c7c6971ed --- /dev/null +++ b/test/resources/accounts-acl/config/views/account/register.hbs @@ -0,0 +1,58 @@ + + + + + + Register + + + +
+

Register

+
+
+ + diff --git a/test/resources/accounts-acl/config/views/auth/consent.hbs b/test/resources/accounts-acl/config/views/auth/consent.hbs new file mode 100644 index 000000000..615aa74b0 --- /dev/null +++ b/test/resources/accounts-acl/config/views/auth/consent.hbs @@ -0,0 +1,33 @@ + + + + + + {{title}} + + + + + +
+

Authorize app to use your Web ID?

+
+
+
+ + + + + + + + + + +
+
+ + diff --git a/test/resources/accounts-acl/config/views/auth/goodbye.hbs b/test/resources/accounts-acl/config/views/auth/goodbye.hbs new file mode 100644 index 000000000..305cccac0 --- /dev/null +++ b/test/resources/accounts-acl/config/views/auth/goodbye.hbs @@ -0,0 +1,20 @@ + + + + + + Logged Out + + + +
+

You have logged out.

+
+
+
+ +
+
+ + diff --git a/test/resources/accounts-acl/config/views/auth/login.hbs b/test/resources/accounts-acl/config/views/auth/login.hbs new file mode 100644 index 000000000..9c167bd63 --- /dev/null +++ b/test/resources/accounts-acl/config/views/auth/login.hbs @@ -0,0 +1,51 @@ + + + + + + Login + + + +
+

Login

+
+
+
+
+ {{#if error}} +
+
+

{{error}}

+
+
+ {{/if}} +
+
+ + +
+
+
+
+ + +
+
+ + + + + + + +
+ + +
Don't have an account? + Register +
+
+
+ + diff --git a/test/resources/accounts-acl/config/views/auth/select-provider.hbs b/test/resources/accounts-acl/config/views/auth/select-provider.hbs new file mode 100644 index 000000000..2c7fa1382 --- /dev/null +++ b/test/resources/accounts-acl/config/views/auth/select-provider.hbs @@ -0,0 +1,27 @@ + + + + + + Select Provider + + + +
+
+

Select Provider

+
+
+
+
+
+ + + +
+ +
+
+ + diff --git a/test/resources/accounts-acl/db/oidc/op/clients/_key_7f1be9aa47bb1372b3bc75e91da352b4.json b/test/resources/accounts-acl/db/oidc/op/clients/_key_7f1be9aa47bb1372b3bc75e91da352b4.json new file mode 100644 index 000000000..3277453cb --- /dev/null +++ b/test/resources/accounts-acl/db/oidc/op/clients/_key_7f1be9aa47bb1372b3bc75e91da352b4.json @@ -0,0 +1 @@ +{"client_id":"7f1be9aa47bb1372b3bc75e91da352b4","client_secret":"c48b13e77bd58f8d7208f34cde22e898","redirect_uris":["https://localhost:7777/api/oidc/rp/https%3A%2F%2Flocalhost%3A7777"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7777","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7777/goodbye"],"frontchannel_logout_session_required":false} \ No newline at end of file diff --git a/test/resources/accounts-acl/db/oidc/op/provider.json b/test/resources/accounts-acl/db/oidc/op/provider.json new file mode 100644 index 000000000..88504f9ec --- /dev/null +++ b/test/resources/accounts-acl/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:7777", + "authorization_endpoint": "https://localhost:7777/authorize", + "token_endpoint": "https://localhost:7777/token", + "userinfo_endpoint": "https://localhost:7777/userinfo", + "jwks_uri": "https://localhost:7777/jwks", + "registration_endpoint": "https://localhost:7777/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": "", + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": true, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:7777/session", + "end_session_endpoint": "https://localhost:7777/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "ohtdSKLCdYs", + "kty": "RSA", + "alg": "RS256", + "n": "sxljNM34KhyDZIXX6mjR0GIbs8Z_IzeBfoFDlkxhdf2Tigl_mCZEnc88fBp619e4l3D_t5GfyR0ZWQuhmCUTY8AJuKqdyuV_jU59nvut_izKydgNxBHGeFMd9abG-PTuq6iE3qEyr8A04KAsZZh2Zact5i6Xvb6N1GB4HDMU3LUAcUwkB6QhCpC4BPzwrTQ8DJZEz1O-_cZj9Y60gFvEo1NCLY6ZppYCfI5wqQhaQJ3jsG0TM03w4w2mcWALrIRoCrt-FIVqKHlKaeiioQALlj3Hdv38hljZtO7FykPqZE4N0nn7T1KQyj2LNCYDU_-ibTwdWm9yagdGuEWPCGvVnw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "gI5JLLhVFG8", + "kty": "RSA", + "alg": "RS384", + "n": "1LvKSpE6v2nhiIdErilavuIRu7aFc3Sej72jtDYAZze6R4dl3-nNnuaNBj8dD3PU16ZD8HQrLKTV6W77udl2yAjar-ZcVpItf7VUX_dCQsRehe7LVC_NgiBVFz88JI_rFF3F2WLC4rIXujv5XdG2v7UyV-KAODrPgY5-jfDJOv11_Klrrpekrtlk98STu71HJYTQR9CzQnMtxBVCOXOIVPVaexnV6gKBSrRtgKHqJxt8FRU3j6xYBwAdeDZaUyeHyUAz2oZkEHmNxoxEj-6yqaTf53AEf3EKbzYHCr4puRJx3H05ZLHkRoUG8utl7CxDsSQPDbwnk2jPufFSmPvR2Q", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "1ZHTLTyLbQs", + "kty": "RSA", + "alg": "RS512", + "n": "uXwK4QaRFmscFO4Sa5nKr5PwL2mWBL9e-omB2cCqqB2V6e7VHq5A_ybEFKXcXDGJKxxc2fHo_PNclUAqIr9Qa98nkQt0bd_F2QxtCqPc-3WcUoe3s3TIVNIOWwp93OAlabBkuNfb7dxnpUjYeGzIs-G7EPhON_5x0h2sC0r3v3Ev_J1mwrR3z9tpUzaODqmI2LKdc3Tu9Ha09CWzb4uRTXC4eVIJoEMxOelvxn6l8CMWLuv2XPaw-pMv33WK4QKfmnwJWO5TLvF2SaYR31oEL3GeG-SwIFTek1xX3cdeNljqsYCzHGHd4PxSqJGI3BPqn55FPCbdx46ZMmlOz_ImFw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "AVS5efNiEEM", + "kty": "RSA", + "alg": "RS256", + "n": "tQxTzwMoSCCRkiNUyp7CABfMZn8LU56axO31ErnW9qDZ4uuPdSO96nqHBU2JoMbnUjFQ9hufAt3UJPHDDD8kNoOOgEZb-CWnb_349oHb8bn7aIOpX1peukndSJ6Nt8SBvbARkb4ErI2b7V9588R8kPwVdW65BAK4ub1lc4EewKJWv4nVIvtp9m_qlohV321rru573hS3BI5qOX2NY1m_Abz4sBGqJVR1o95MqR2IYUeCSORPj34GSdHNUipMVJrouI7LAoO9dNhCu1q8Efy-Sn1YuCgEyTy_AMDuVgBgf1AHssXRymbE6A_IKys2ZxYZPYAUZflyffdUX9qmhtACaQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "ZVFUPkFyy18", + "kty": "RSA", + "alg": "RS384", + "n": "q1VCGGAL3arQ5tG9vMefKaHC9EXJLlJYu7Lgk_8RBPKJ8yejiTkU7xRWwJowK9kLsyYTHvCsJlGc-phNyEAE58QoqmQGePbr80H_q-7fF3H85UsQ5XFg2A06KQYT3dLn57Qzsf-qlJKwrVR3Rrz1XoxYY-IgEHPad86xW5PlwPKiNY1ZaWsOjdeBccsgfCeG5tn13a3GY5BoX90w7b8ly_BsL904-_Yeog_deesQ39oE_XKpORBnDxvUjFtPJIpaPMCHEqAKiVXH_dZrcGUevs9xvDl2Odiku7pUam3atzNQENKwB9HMDjATVdYZfelF9cllDVAUXKFHMenwqoV7PQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "mROehy7CZO4", + "kty": "RSA", + "alg": "RS512", + "n": "xq5YLUgQU82zwNtjw6xTbkAxpTp56_lnLy33srWPlZeFypbT8p2hC-918Vn08j-NuvzUeqarFLv7xBUZWrV2ilho_IYQWBZdMYCraBtDoLglctJtb6RzRG7rF0KsiyxsTwLCZ5UwcGpc_ZIrcDTRkmHvgqfA-KKpK_hIAwGC7rwNPDK0E26vuiaH85wnanQaWfdHzHFPW-cUWFbmjOZIQh0XHQSPnjE2JYX7rWWKga_8Oq7CUF_ArEF-8qTGL59GwS4OFcilvwyb53ANHy2bOEidRZCGQo6Kh2EjyjBHNB_YAiOLwfeTstb0fbDfWbfmdO3lW_-lBuGnQMPY5ukG5w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "it1Z6EDEV5g", + "kty": "RSA", + "alg": "RS256", + "n": "xZgfW761Mad1PHSOINv6kU13aiueEn19Ko3CR5EiuyuO5v8uJMfV24Mg8JTOxq9GuLIzte4CMg-5kFxQVopkqYZ8TP3eHAW6kWbh4j-C3I8vUJiA6LWGblFUsMg_sWvjwMPK4oF2bNeqGSVXSOtg3PMuBdV5wB4IBDikTovgSSbQ2gfgkEil94jOh0_bjzDXMDH6dv8Ong2Fj_bfWUg3MKcm6yVTCwCfqlfgNpcEqm6m3SVVpQRVxvlbsPmBt61w3QgOwC68rTD1BRVsH_DyL8DVsQKsg3PAbKqqNY0HM5YS9VPdFFxKYHiX7hZuNmtcPWDStxWSQNrvn4aaw5Xi5w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "v-cHHQPNDvo", + "kty": "RSA", + "alg": "RS256", + "n": "sxljNM34KhyDZIXX6mjR0GIbs8Z_IzeBfoFDlkxhdf2Tigl_mCZEnc88fBp619e4l3D_t5GfyR0ZWQuhmCUTY8AJuKqdyuV_jU59nvut_izKydgNxBHGeFMd9abG-PTuq6iE3qEyr8A04KAsZZh2Zact5i6Xvb6N1GB4HDMU3LUAcUwkB6QhCpC4BPzwrTQ8DJZEz1O-_cZj9Y60gFvEo1NCLY6ZppYCfI5wqQhaQJ3jsG0TM03w4w2mcWALrIRoCrt-FIVqKHlKaeiioQALlj3Hdv38hljZtO7FykPqZE4N0nn7T1KQyj2LNCYDU_-ibTwdWm9yagdGuEWPCGvVnw", + "e": "AQAB", + "d": "pv-ULqejr_iYV8Ipm2yTv3_Lnu0GnZrjB0eW8u1Tr0Z8LSlALWn5b0DOgFXcl6iRebym5M9Hs6qLeSlMS2a-1rM5HVUR_x_RuLwojHbXPXsct-raoymD66xs8iLJw1f3uF5RTpn2fkR1ycHww-bO92hUdx6Y5Rdqfk5ZkMncuRIJI4PHrYcSxaGogl5JNL_Bzza5Sb8-GGV0Ef5wB9S4CM2VUgLj2r5RzwpezcrIA0w9TnbtEdA5EEdHG997jgQhp-fSUPKMtKrRRFJy_JqIYRUi4SOLP_gJYO_qpJlb9pxVQMVnhhXTnso-pSCfsxCTxRjb176BahlG3kuNTiwXKQ", + "p": "5JrtuYCK4-apgRriDLC2_LpVjlnioLoHHUGyYh8SZPwpOzDoQI3EOIZyFM0X9hRMBWoNXjgCUGhdwwAfw24JgKSx_Obni3pRVz69skm-Ee1dCRlDGi91B9q3-cNJG0qJI9mIPIRp2PCCvXToC48PVDkBm3t7zdzRPaosu_YWkrM", + "q": "yI-68nioykS5WrcvjKpsGke7O7MZ22sj9EGtPBRgoxSrDzZK9MutnM_9_vMYPGZy1cN8Ade1-Jw7qA8w8ZESeu5E4cQkArgpdVG34EEDz61A5SYf4GkD-qJ803TxZcmfqfGX-REoKUNafLaNbhQsOHrhrdN2oH-CZq2KrVHCt2U", + "dp": "zMGn49sqi-5yLF0z00IE5GDReOsxfdyhuqa5bAGArErfc1De9dMEycxCKjd5GsQbQ042IwnvqK2SLbLSwGyyvjLF6Uu4YMlySb68khBS2iPMjPW_kJipLhvNZTxxIqykISQaTnobhGAH-kHYBWJhzIIy2lzECyOZlq3x23kTxtk", + "dq": "etoP2ZavTbbrEvZC2hdKQI7P0bHTlOP8EhJo2vRgfYSbg6XuJCTfI78EBrdBkT3v-aDUxQwtGywYHsmvYUlL2KE68FAE_uVv_70etO8eNogZyEOiIwQwu8XsUFrBw2fNtXuXa6lmwF_RfbMUzujsbWxX8PInKAjzB5Il8CS08UE", + "qi": "GBJ90AkXHhbgiL4yk9w6MtQxi1F8XRHBpG3t97Aj1we14pITY56vpEJi97gUjsRsH9DZqzIFV62CSF0VMWaxxRX3c6yuUtJMBSq9Skpvipjwatlz3jxHGP26IFSO9b-NpidM9_egK5mYlGuNY0N1CN-7Lw_Rpt8cvrvvi2tB41c", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "ohtdSKLCdYs", + "kty": "RSA", + "alg": "RS256", + "n": "sxljNM34KhyDZIXX6mjR0GIbs8Z_IzeBfoFDlkxhdf2Tigl_mCZEnc88fBp619e4l3D_t5GfyR0ZWQuhmCUTY8AJuKqdyuV_jU59nvut_izKydgNxBHGeFMd9abG-PTuq6iE3qEyr8A04KAsZZh2Zact5i6Xvb6N1GB4HDMU3LUAcUwkB6QhCpC4BPzwrTQ8DJZEz1O-_cZj9Y60gFvEo1NCLY6ZppYCfI5wqQhaQJ3jsG0TM03w4w2mcWALrIRoCrt-FIVqKHlKaeiioQALlj3Hdv38hljZtO7FykPqZE4N0nn7T1KQyj2LNCYDU_-ibTwdWm9yagdGuEWPCGvVnw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "5Rhg743p3K8", + "kty": "RSA", + "alg": "RS384", + "n": "1LvKSpE6v2nhiIdErilavuIRu7aFc3Sej72jtDYAZze6R4dl3-nNnuaNBj8dD3PU16ZD8HQrLKTV6W77udl2yAjar-ZcVpItf7VUX_dCQsRehe7LVC_NgiBVFz88JI_rFF3F2WLC4rIXujv5XdG2v7UyV-KAODrPgY5-jfDJOv11_Klrrpekrtlk98STu71HJYTQR9CzQnMtxBVCOXOIVPVaexnV6gKBSrRtgKHqJxt8FRU3j6xYBwAdeDZaUyeHyUAz2oZkEHmNxoxEj-6yqaTf53AEf3EKbzYHCr4puRJx3H05ZLHkRoUG8utl7CxDsSQPDbwnk2jPufFSmPvR2Q", + "e": "AQAB", + "d": "vvMZLzmQ7APUR0Jz6YiBRdmSZVX-D5ZcRVXJvZbDYeLpuA7W6Nfqk3kKmNLJ-PbV1AQP86OypU4IHJJcLYP_VKpt8Xnq5GItqPZQmBtPRLMSzVF8_UIzS1xORKGkEIWGUy-gyfIWUHnfRnFS8l2tlgLE_5H12YMgg4AuJKY_WkxJSedTKwr4K0COthvbMREqIGbNg9JJhJh54K2FtuNNqn4iycaYCNveunWekRBMpzL2IGsjECGtI4NSrjtneWpIY71pggG87QGduYGVgbdBYFSnJlgbCjN7bQNzpI8v7uE4eM7q6tphJMasVjCS1TGIuNZDl_-vfyCySkNlSvIyAQ", + "p": "6sPjPxcGVwAX1ADLxs7YRN_1U1xYUV_UenzTAnaNac5W8s-AQDoW7_6oCD3s0EmBRWsT_jhGbDUyMgJa0ZASa3nJVqXdYTrrxaBcOktUpLvq2cRgcxLkH_CYdT6yQMeUIjnAg5z-Rkjg0lvWPvqi-IVKDcoFUuF2sjGJjeF9d3k", + "q": "5_m_mSjbVM9ZGvvr-XDAybD3z2JPft1PjCISHcNdTe0-gu4z7VXNnIgynhD0JIee8UpEnBrPFOd7raPxY-y4wdYF-zE3gvl9IOveG793uPctvbWtQSYpcZuPWodn8t-3LvZNq5kLZLCSUIrgTJiwIS7v5Ihc5fxVuyJSYHeBtWE", + "dp": "4yrZ4lqtT9JPPF3o0V-l9j-gbCGXdGZ-fGf85w1AmXmIuTwApiWPvHt2rUL-vC3kYP_UQNLDkkGHaMzOhKocqNMX-DhXl5YkPv-FPwNVzHHqNv7HNZK6HA37-LfKVNTKirPHjZOEmQ48PlGPZzGwMTsJBX7O1_xDlvpIWHoxpkE", + "dq": "KQC8HRZbrmH4HgzpaO3FJeFh7AY0hvgXV22uRhSCKYQFyJ7SDuFbto9cYxQcE1jlf0DhX7ZdZBSGh-qygDcXcSujYwMQDNaMh4UpfT4aq1cFfsLeHOXh7XLRo-7LMOLaPjLLB8nFeca8FgB2JRPYDgV94ac4xG4VuT4X0XVOOAE", + "qi": "gVHniXGKh_ewcrZRRei-ujdYm-htsGYGjmCyXXQ_RVJYz9tauSzmBQPGfE088Wp4ybyTv0exZ_MnizFDHIpP6TWt_Dg5uYWP2UHbKdwdAs8nA9NSXdUFtyE06HsYx-Rd8APYl6A0oCjENweAx7xq9R4zbdMdZpmpX8v2N5WSZN0", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "gI5JLLhVFG8", + "kty": "RSA", + "alg": "RS384", + "n": "1LvKSpE6v2nhiIdErilavuIRu7aFc3Sej72jtDYAZze6R4dl3-nNnuaNBj8dD3PU16ZD8HQrLKTV6W77udl2yAjar-ZcVpItf7VUX_dCQsRehe7LVC_NgiBVFz88JI_rFF3F2WLC4rIXujv5XdG2v7UyV-KAODrPgY5-jfDJOv11_Klrrpekrtlk98STu71HJYTQR9CzQnMtxBVCOXOIVPVaexnV6gKBSrRtgKHqJxt8FRU3j6xYBwAdeDZaUyeHyUAz2oZkEHmNxoxEj-6yqaTf53AEf3EKbzYHCr4puRJx3H05ZLHkRoUG8utl7CxDsSQPDbwnk2jPufFSmPvR2Q", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "WHTKUBTBjl0", + "kty": "RSA", + "alg": "RS512", + "n": "uXwK4QaRFmscFO4Sa5nKr5PwL2mWBL9e-omB2cCqqB2V6e7VHq5A_ybEFKXcXDGJKxxc2fHo_PNclUAqIr9Qa98nkQt0bd_F2QxtCqPc-3WcUoe3s3TIVNIOWwp93OAlabBkuNfb7dxnpUjYeGzIs-G7EPhON_5x0h2sC0r3v3Ev_J1mwrR3z9tpUzaODqmI2LKdc3Tu9Ha09CWzb4uRTXC4eVIJoEMxOelvxn6l8CMWLuv2XPaw-pMv33WK4QKfmnwJWO5TLvF2SaYR31oEL3GeG-SwIFTek1xX3cdeNljqsYCzHGHd4PxSqJGI3BPqn55FPCbdx46ZMmlOz_ImFw", + "e": "AQAB", + "d": "nRMhd1yDQ3PjLQpLSRnM3hEu5kfJBi41tX77GrchDgs36AocKsYwPqLKjB3FVcGRQpPbQamtv4ArmCzlQdW3uhIZRKhpqZ5Fwr_WG5uWyM_ZWL6b33n3KHVWONzSp1id9jmtoicSlQUANKVSw_CDqmlvbDiKrLpqEyCTkGClG1XCMpTRq0IA_D19ZORd3XvdBePN1H2djX9Lh6ODW39iVdoDkj8b46STakIbu9rHUwA8ZusGd671JnXB4OemX71MCi677_GN1r5buWc8puFV8mrv-kYfk4hPyXQqZAqo9AbgoNbRb62OoWhs5mzmPYoxLyGNeUOedqefmSCQbQl1gQ", + "p": "5yAwbWvBFS3Wtgd4ncPQRkEqPVjaKU3u5VWdytdZylkGNVfB95WJiBJmLa1_arlMmKLuZlHAzgNDmd7_R0F6Bd8_mLynaxk3MTzsrawk17HTXkPX5k9jm8XDc1F6wvK9kL2Xc41DCvalWt6QMQXzJdQNJWy-mJx0He5CrULMHNc", + "q": "zXJhNeBWNg8s9QGufel8Mlewbu_e2cQVsolZZOgXlkj8_IbeRzH0PeHbzbSmabv4tJ36X579ddK5MSpL81sZ5ZbuPFYVVJCb4jzVtDFfNcgkM0OfRj_2F_T1JI2H1WKwHowTyQiXVp8xrECUg0DzkMpH-lse7fkrrS0-Vne92ME", + "dp": "lF6Wl_efWJA3kF0Vcfmc_yygCAe87N0JqhEfHXLHQl2J3b57VwuY4VAmZdZFwGY5pJabgfWjVtzDjciYic6fnZtmAQ_CTb8_Lg2VRhwG_qw6Kv5UX5XBNONsh9_bdcBMLtl2mwgo7KXPGplbaQ0PvM32rnqzk9aDuB8WkJEb5Ls", + "dq": "X7WQaev33blWJVHCO3BBZqaJUDU5KVP7E7B-z8575oxcJzyhYqN3-Dg3EO6-s_VY2LPcBx3nUDN6CNh-h4GCX_3fQIaN61Zu-IeEuyxhAYoaqzMuiSiU-fYpGf1BMXyHNcPmF7qD3lvNZUS0qyzgCyzhOVWn5A83dLbmGpwv-kE", + "qi": "pD0kXsVUjZWnDoExmpB2QnQcSP_op-OPebrLqHVzsXBZfpkf4Do48yrnL0BjI825008dDDq3fHXxWR42Vc27zHDvkaqg9ZJpCQIOpY2jKT1jYZ-HYqQeqvXCDSHM11hfkce0OaBGhcWCKaOX3-wB8sDmD-8K3DpCTuplXCGBeWU", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "1ZHTLTyLbQs", + "kty": "RSA", + "alg": "RS512", + "n": "uXwK4QaRFmscFO4Sa5nKr5PwL2mWBL9e-omB2cCqqB2V6e7VHq5A_ybEFKXcXDGJKxxc2fHo_PNclUAqIr9Qa98nkQt0bd_F2QxtCqPc-3WcUoe3s3TIVNIOWwp93OAlabBkuNfb7dxnpUjYeGzIs-G7EPhON_5x0h2sC0r3v3Ev_J1mwrR3z9tpUzaODqmI2LKdc3Tu9Ha09CWzb4uRTXC4eVIJoEMxOelvxn6l8CMWLuv2XPaw-pMv33WK4QKfmnwJWO5TLvF2SaYR31oEL3GeG-SwIFTek1xX3cdeNljqsYCzHGHd4PxSqJGI3BPqn55FPCbdx46ZMmlOz_ImFw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "wrLgRGiRzLQ", + "kty": "RSA", + "alg": "RS256", + "n": "tQxTzwMoSCCRkiNUyp7CABfMZn8LU56axO31ErnW9qDZ4uuPdSO96nqHBU2JoMbnUjFQ9hufAt3UJPHDDD8kNoOOgEZb-CWnb_349oHb8bn7aIOpX1peukndSJ6Nt8SBvbARkb4ErI2b7V9588R8kPwVdW65BAK4ub1lc4EewKJWv4nVIvtp9m_qlohV321rru573hS3BI5qOX2NY1m_Abz4sBGqJVR1o95MqR2IYUeCSORPj34GSdHNUipMVJrouI7LAoO9dNhCu1q8Efy-Sn1YuCgEyTy_AMDuVgBgf1AHssXRymbE6A_IKys2ZxYZPYAUZflyffdUX9qmhtACaQ", + "e": "AQAB", + "d": "iI67yDEBeSXXpvqvQgVtHtTUf5rj2DaRVmiFqZIy6eN5dQdoNVq4LNwua3mIjZR5di1se7Vpwqe_E_6mt94IWnXwTiDDze_Y00glOQnJ9BHr53Enl5x6Rtjf555wFmRJ1-Gt3tgMfnpxWiHhwlQ6AMGjDeht9PB4lOCeXPjPUUvbkKKKBWBtVw-8e9hPZdJFjmMU_bmYL9i-gXMf6xWn4JLkrO-lVDvAqG7jlHdFN49HFBxFuxw-T4DY0GTd8OfnOBSWGaleADncTaUKL6dvXwgNtnes_PPKUfJ6BTgYpmM_4HhWMuuosarxhJAwkGoWu7LRm4W_jy5QUDFIVqTj4Q", + "p": "6MBN0ZdNba70Y3lEijgyYDE2oFtLFs3b9HtmLpr4_vQ-b0o4iasQO5bYmVW54rDvP_rCyBDs7uZUvoqeYD-xRYiPDErS5AzoeVNDoFS29fC2mNVPSqNBFOcRnqSMStuvAQwYR0zkYuCz1paAbLTZuiEmamNKx9Sxt4-FrEq6uqc", + "q": "xyHr9MFcb5VYir3d2_yRs0glIk_LNgT5uqv6R2I49iD-Z-w6EBen7M1ttkqXWA3J_kIufM75MwDjTpOFjO1Q7GVCVV5T4W9vs34Ko3u4jPJziECeIFV1ZDfyHk813eGhaGh9R_oqHe47vE2wBeRPzpIWj3ZG8yOrSTbn7eOEzG8", + "dp": "4zfRAHqPuTMiG_YoBjOEYknJBVT6giGnyA2rnHXn_KWeSfEQLr2UFEhX3aFF3dtTRYddHgj_9N1g_769jELBoZsF4z8skDtVvBOgImZxUrmS2LLtPHURtQE7Pz9uQioit4gCL6EOGMU6a5Pzfaw0HbP9F8ElIN4wPH3dRmyRzGM", + "dq": "iqVFog4XC-HR2hfEJuy9jTQIFtGzzRK9xYkEIztyKXxjZXwGGTo_QxLs9mUM5tQC9bKip2d7_lT57rWr4KlDFLST8NhSUr3B6hkx0w3LOud8JTvIXP7jUznYq92-xZPZS9akk77MIDbFBKCalB-YqVzxtEVHtPX6xmkiJnGo_qU", + "qi": "ZqcNWxzQ7lI4JxsQQhKTFAghR6J7QJMaqiiTrUiaWOSlB33kRKEdv3s1LAfMNdbGr3zl-Buhj5LOX-tPvWSV4ua9GumiHOr90Nm_WiTAJT2gbtXKToaJHSk_BeKN_8feak0Mvzwxphv8xz6C96NbXwDIDTV5YQweRFvQY5Mpmho", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "AVS5efNiEEM", + "kty": "RSA", + "alg": "RS256", + "n": "tQxTzwMoSCCRkiNUyp7CABfMZn8LU56axO31ErnW9qDZ4uuPdSO96nqHBU2JoMbnUjFQ9hufAt3UJPHDDD8kNoOOgEZb-CWnb_349oHb8bn7aIOpX1peukndSJ6Nt8SBvbARkb4ErI2b7V9588R8kPwVdW65BAK4ub1lc4EewKJWv4nVIvtp9m_qlohV321rru573hS3BI5qOX2NY1m_Abz4sBGqJVR1o95MqR2IYUeCSORPj34GSdHNUipMVJrouI7LAoO9dNhCu1q8Efy-Sn1YuCgEyTy_AMDuVgBgf1AHssXRymbE6A_IKys2ZxYZPYAUZflyffdUX9qmhtACaQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "1IGzLGffBQI", + "kty": "RSA", + "alg": "RS384", + "n": "q1VCGGAL3arQ5tG9vMefKaHC9EXJLlJYu7Lgk_8RBPKJ8yejiTkU7xRWwJowK9kLsyYTHvCsJlGc-phNyEAE58QoqmQGePbr80H_q-7fF3H85UsQ5XFg2A06KQYT3dLn57Qzsf-qlJKwrVR3Rrz1XoxYY-IgEHPad86xW5PlwPKiNY1ZaWsOjdeBccsgfCeG5tn13a3GY5BoX90w7b8ly_BsL904-_Yeog_deesQ39oE_XKpORBnDxvUjFtPJIpaPMCHEqAKiVXH_dZrcGUevs9xvDl2Odiku7pUam3atzNQENKwB9HMDjATVdYZfelF9cllDVAUXKFHMenwqoV7PQ", + "e": "AQAB", + "d": "qLX9ra72QELyDjg-k-Ql8ILfTFZjsn9X7QxJZLJn-e0ytgM0X-2blYj7nBC4mpTRlolJjtADVIBNCd5ryWJw8iAQwyhXz0mmMhWtQ4qml5mhI9B1RNoOOPFdcgQQEACcZ2bk-MP_HuoLm8Ju6XMsUvv0FfcXB9xtJkicEMdkGEe3uchr384r5t38ffaC-8ZA9enSoHBZwRaxFlt3i1TAGFwwQNeIsssrXJrUXi-YlZqmXaRf2Gl0fboXboFLXaWTN5RfD1iQ1zUBg55XswpkJhyR6D81XZLrTK-jOEbrhrclj5jujtk5TeqYrIZtMBNUwgRGzFkczkcNCWilFqX0aQ", + "p": "1RzqSRl2tZQrvDYVJIkufxtI-GvXVjIZYM2TUkCinAoHKN7QlwwL0QAXamr144v9JCGbMEIjcFo16Rj1Py77jLjE15ybdZpHqz3Gy_htjp0ySHJMI-T5Bxm5JxuPQLYj3k9Bhik-HcsQxJHKPXUZqpDDh-ivySd4UuGBpKOZXSc", + "q": "zc_wXz6sqrSHQPH6Yrr6oVJPmvwzBFv05g4NvWwoATZavuGo2-BdkqZVVaTPwBEB-BBgWz_VBhn48sV0gqN6mZOI9897HraPIwoNX1eWvfqPliMmbj9bHB99ZZtPqLcA6JXt3pISdE8mfEUHm65tUdvZ7l9wlU_RcHXdOS_javs", + "dp": "K9w3m7PR6q0EE0hOMabKGv7Slc4cE3FcJ8AngdYroVGvB4pUA8JG7EzIhO5ejOZSwwznk5cJFCZ80eyBDO_udZfRa06f8CRAe83LDE-kvKU9pAtiAEEvv3Zb1OCnKvpRh39oTORQFHGmkc4vgVaIYcJJe7837n5hFS20MN46wiE", + "dq": "mC1qZGJpNYdqgqDpLFtouiOsbMKRzmVX_Uri6e6w3cSc8IrWWk3ZoneOnVbRrghlVlB1jsLx9iL6KjfJ4FaUbj3ihqlJNfpyd8wU-yw-b5Z22OKApf_-lBrMk3Z1PiCicVd6nJmRP6LOqBA6gehFOMPArjqvehecmvTrcD9yfkU", + "qi": "BAG5sXbnpXWa0kUNCFgsX6YREYvSkrdeCLnpUHSw0ydU9xLswRBiQaYjoTWNHG1IfiSU-ascFqW-xZGlTEi8HDKamxZqYDyxvUMpYvSOleeMEK7Ieq580FQlzNHQ3supNMr6WK0cHsxs0dw3MBFkI4k7QknB5-mOLNvPD-F57Dc", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "ZVFUPkFyy18", + "kty": "RSA", + "alg": "RS384", + "n": "q1VCGGAL3arQ5tG9vMefKaHC9EXJLlJYu7Lgk_8RBPKJ8yejiTkU7xRWwJowK9kLsyYTHvCsJlGc-phNyEAE58QoqmQGePbr80H_q-7fF3H85UsQ5XFg2A06KQYT3dLn57Qzsf-qlJKwrVR3Rrz1XoxYY-IgEHPad86xW5PlwPKiNY1ZaWsOjdeBccsgfCeG5tn13a3GY5BoX90w7b8ly_BsL904-_Yeog_deesQ39oE_XKpORBnDxvUjFtPJIpaPMCHEqAKiVXH_dZrcGUevs9xvDl2Odiku7pUam3atzNQENKwB9HMDjATVdYZfelF9cllDVAUXKFHMenwqoV7PQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "zVgFtyyWWik", + "kty": "RSA", + "alg": "RS512", + "n": "xq5YLUgQU82zwNtjw6xTbkAxpTp56_lnLy33srWPlZeFypbT8p2hC-918Vn08j-NuvzUeqarFLv7xBUZWrV2ilho_IYQWBZdMYCraBtDoLglctJtb6RzRG7rF0KsiyxsTwLCZ5UwcGpc_ZIrcDTRkmHvgqfA-KKpK_hIAwGC7rwNPDK0E26vuiaH85wnanQaWfdHzHFPW-cUWFbmjOZIQh0XHQSPnjE2JYX7rWWKga_8Oq7CUF_ArEF-8qTGL59GwS4OFcilvwyb53ANHy2bOEidRZCGQo6Kh2EjyjBHNB_YAiOLwfeTstb0fbDfWbfmdO3lW_-lBuGnQMPY5ukG5w", + "e": "AQAB", + "d": "taDca0jd5D7AfSYS5ea7vqZgvDPhEaBGfBMBxhXE1XRXkwSfbcQ8XbTjlWgvTOZcovxPInELJUFUv8SqEQqi-4YnM_M7LcwEFiUSjXGfOWYelgFYmh80YPMlZ3ZEVlaeDPzwy9DPH3Wc3RKrM0CV9cQiOMcy2hmZneCztEvFbohMI8bXFYeZRA-i7qJH9N3Cj_9iqGlKqnSEBl59IJX6FacX8EVi6FwCXWpJI5b6afab0dHBeZBjN-ZqRtR_kf78gaTSUKySJNrCoXpAun2HvYFXJYrt0byWho9wKt5x35SF3jcJ-DwEzjlCP9kZfw8XVPORh4tXKlbu_IrH0Ia-QQ", + "p": "_jIqpz116Ae6tqpH_HN5eT-ywJOHN_RJWbETsguBEWXjxJFdsP_M_34Rl1_T2Cz97iqde40IgkiCw2naUupwDdzY3DrmH0l8Z5nM6hyteRS14Y3z3GhX9Z_3BdsLSd76gpQdbN2C8QlG8OAHW0xT-6vYwo2sYHhEdBdmnBGIs4s", + "q": "yBdQ6sU6Px5M4sL3KR9cBTxOXJit-9Y8wdHPaSbmAZY-zVXTBWR0geLG_Dkx_c3NncSrSwWUlSVjLg-MG0EWr1W_dEjBWvAFjvCRqNoaZNFkOU_j9LG6zVyR6XPihENglaYZF3pKVDOjSqT2j0OIztcenHxd3sTE0BVBvEoedZU", + "dp": "3Cwdr7_XaYOQYPl64pouhCv9Kzpda8TG584t7hBy2dvz_eWfTlkyebX7jK7u8hZ-V5VH1KUi0p31zUbZWOpA5nD80Tye6EihXabky37NbsvWgiiPKcCjN1g4ATVqQLDHMOUT26C98wMDFE4ncRfawmlllZZa0TA6soc2VEYHruM", + "dq": "VIKEiqQCleYWUzBFc_jqxMtTzYgu887omnQjRiZHvyPWIqO9HOnwy2sc4CrIEop57cjDEEyrFNNVsH6gjmJPUn7E_jg8ckwuDNFOtCJqQ2qtCgfUH-VxIIuYlSF86qAKiyo8Ls5X1nh4324NNTUw8yuooi9k9lHlTn2r5froIoE", + "qi": "1YMuA45Yn4yHMa_B4xbRdnXdKehWJlSiksNfTbNINUqvLwOQDhCqVaPoamde4tS2nzT-ZQTxrp5jQqFGjgjTm0-p2EIFdzjs0NtLMDeEuMiHaxp7Ov1LpjdffTn_WknFgQtkjgygg2e5XQrEWDSzqNeV06blIbnegk1YnE6c8Lg", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "mROehy7CZO4", + "kty": "RSA", + "alg": "RS512", + "n": "xq5YLUgQU82zwNtjw6xTbkAxpTp56_lnLy33srWPlZeFypbT8p2hC-918Vn08j-NuvzUeqarFLv7xBUZWrV2ilho_IYQWBZdMYCraBtDoLglctJtb6RzRG7rF0KsiyxsTwLCZ5UwcGpc_ZIrcDTRkmHvgqfA-KKpK_hIAwGC7rwNPDK0E26vuiaH85wnanQaWfdHzHFPW-cUWFbmjOZIQh0XHQSPnjE2JYX7rWWKga_8Oq7CUF_ArEF-8qTGL59GwS4OFcilvwyb53ANHy2bOEidRZCGQo6Kh2EjyjBHNB_YAiOLwfeTstb0fbDfWbfmdO3lW_-lBuGnQMPY5ukG5w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "wySK0UGZma8", + "kty": "RSA", + "alg": "RS256", + "n": "xZgfW761Mad1PHSOINv6kU13aiueEn19Ko3CR5EiuyuO5v8uJMfV24Mg8JTOxq9GuLIzte4CMg-5kFxQVopkqYZ8TP3eHAW6kWbh4j-C3I8vUJiA6LWGblFUsMg_sWvjwMPK4oF2bNeqGSVXSOtg3PMuBdV5wB4IBDikTovgSSbQ2gfgkEil94jOh0_bjzDXMDH6dv8Ong2Fj_bfWUg3MKcm6yVTCwCfqlfgNpcEqm6m3SVVpQRVxvlbsPmBt61w3QgOwC68rTD1BRVsH_DyL8DVsQKsg3PAbKqqNY0HM5YS9VPdFFxKYHiX7hZuNmtcPWDStxWSQNrvn4aaw5Xi5w", + "e": "AQAB", + "d": "pIK2-QeajDDD5wWTn8AGqhs5JOgTv4lDQL6t1i_8HqFxZNloba8DWrOeJS9_yOP9maCkdQAoS83TzWFOcf7fOFWEAAYNen86ifyCbIA8T63W0t9l1FnuBsMoI9dVUD5nbQKWVGc9Vflo4W65cTineM3ur2TA7TcTrZALHGpQ3hU9hSLPzPmazeeNKSEwy-euD3Cjm85FLdlNHrk6Leb65zbOs6fumxwUVaBq-KmyK7EerUPeAUh0K4Xy0BFt1L1x9XI4unZDG4HfR177eDS_vvL_N20KzFWZvbWJeuiwGZn2NwIeaA0kIcVHpd3gUrEy9DaV4tsrfhsUZb6apylSgQ", + "p": "_9tQNveciKNBxgX9GepZ3G5VLMQhjkvInIlA-seE2moudpsPnnZqk2ZEc0Zl7XTeoTv1fBczUZx06H4hj0gdAhkHPLUJz0YtasXyRSX53aDICacj4rJYw78a-eSJ3tBKkbDV0Q24MkDY3p3MlVAAycxwLS0wHPc7GPQwPa7K39c", + "q": "xbR0fb0vrARDTZB51sU15L9tzSvPNwkt1O07lZolgoFdDgX_0ADgqv0iHgSlBQR9hoKHTqeEAjbkxRHBmv2KIhH_cLcESMU4JkTs-j1kz5diprfuutWWvs57XjCvewbbp59l3lZFc54WeXjzBWTSxvaXTlwBlCwJHAJiF1Dw83E", + "dp": "SdcfpV183apQNzhPPYV2_bkR9-N607hnY1XxXO7sFqUCV9SUg2UliPjA1IwCqq9J-Tp2tKN1eh4vV1HfmZx0UsCqaAjPlfRo8yHBs9cr75yRXsfQAYL7PzMONASTDa0LeFSSwMy21joE3OqpuoXmVFceIMuj0RhBBAilS4gAoO0", + "dq": "Zd1XlB2w_Vlo8AL7s9wCq6yyP19OMdYp5iahZ7B3mSlcL8iJiLubBp7MQFk2SUKKBo8kdjM7ggSUlLFUZq4xyOIrEgFKVNBA4P7sdvbBBXDDpJDqkRtRw1gSGnLNR38-F7y6OPeMa0jN3aKi3GmZbGhLh1VCfvy9aNAViFvs-hE", + "qi": "xy8cIuP9Y_vwX7mOYftqv_NofI37EEBxPdqX-CeEIigflsbmaWSVADql6t-XODgK7PcbepRpxcx4AuRPBGFULvPNgEGy5YtdSSF8RwNt3GhK_d5Hh71-hs0WQ_dZ5yFMXJDTg2RpcsZwn65mN1gcc7a7qYZwciYsa1Ynmj36xmw", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "it1Z6EDEV5g", + "kty": "RSA", + "alg": "RS256", + "n": "xZgfW761Mad1PHSOINv6kU13aiueEn19Ko3CR5EiuyuO5v8uJMfV24Mg8JTOxq9GuLIzte4CMg-5kFxQVopkqYZ8TP3eHAW6kWbh4j-C3I8vUJiA6LWGblFUsMg_sWvjwMPK4oF2bNeqGSVXSOtg3PMuBdV5wB4IBDikTovgSSbQ2gfgkEil94jOh0_bjzDXMDH6dv8Ong2Fj_bfWUg3MKcm6yVTCwCfqlfgNpcEqm6m3SVVpQRVxvlbsPmBt61w3QgOwC68rTD1BRVsH_DyL8DVsQKsg3PAbKqqNY0HM5YS9VPdFFxKYHiX7hZuNmtcPWDStxWSQNrvn4aaw5Xi5w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"ohtdSKLCdYs\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"sxljNM34KhyDZIXX6mjR0GIbs8Z_IzeBfoFDlkxhdf2Tigl_mCZEnc88fBp619e4l3D_t5GfyR0ZWQuhmCUTY8AJuKqdyuV_jU59nvut_izKydgNxBHGeFMd9abG-PTuq6iE3qEyr8A04KAsZZh2Zact5i6Xvb6N1GB4HDMU3LUAcUwkB6QhCpC4BPzwrTQ8DJZEz1O-_cZj9Y60gFvEo1NCLY6ZppYCfI5wqQhaQJ3jsG0TM03w4w2mcWALrIRoCrt-FIVqKHlKaeiioQALlj3Hdv38hljZtO7FykPqZE4N0nn7T1KQyj2LNCYDU_-ibTwdWm9yagdGuEWPCGvVnw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"gI5JLLhVFG8\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"1LvKSpE6v2nhiIdErilavuIRu7aFc3Sej72jtDYAZze6R4dl3-nNnuaNBj8dD3PU16ZD8HQrLKTV6W77udl2yAjar-ZcVpItf7VUX_dCQsRehe7LVC_NgiBVFz88JI_rFF3F2WLC4rIXujv5XdG2v7UyV-KAODrPgY5-jfDJOv11_Klrrpekrtlk98STu71HJYTQR9CzQnMtxBVCOXOIVPVaexnV6gKBSrRtgKHqJxt8FRU3j6xYBwAdeDZaUyeHyUAz2oZkEHmNxoxEj-6yqaTf53AEf3EKbzYHCr4puRJx3H05ZLHkRoUG8utl7CxDsSQPDbwnk2jPufFSmPvR2Q\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"1ZHTLTyLbQs\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"uXwK4QaRFmscFO4Sa5nKr5PwL2mWBL9e-omB2cCqqB2V6e7VHq5A_ybEFKXcXDGJKxxc2fHo_PNclUAqIr9Qa98nkQt0bd_F2QxtCqPc-3WcUoe3s3TIVNIOWwp93OAlabBkuNfb7dxnpUjYeGzIs-G7EPhON_5x0h2sC0r3v3Ev_J1mwrR3z9tpUzaODqmI2LKdc3Tu9Ha09CWzb4uRTXC4eVIJoEMxOelvxn6l8CMWLuv2XPaw-pMv33WK4QKfmnwJWO5TLvF2SaYR31oEL3GeG-SwIFTek1xX3cdeNljqsYCzHGHd4PxSqJGI3BPqn55FPCbdx46ZMmlOz_ImFw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"AVS5efNiEEM\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"tQxTzwMoSCCRkiNUyp7CABfMZn8LU56axO31ErnW9qDZ4uuPdSO96nqHBU2JoMbnUjFQ9hufAt3UJPHDDD8kNoOOgEZb-CWnb_349oHb8bn7aIOpX1peukndSJ6Nt8SBvbARkb4ErI2b7V9588R8kPwVdW65BAK4ub1lc4EewKJWv4nVIvtp9m_qlohV321rru573hS3BI5qOX2NY1m_Abz4sBGqJVR1o95MqR2IYUeCSORPj34GSdHNUipMVJrouI7LAoO9dNhCu1q8Efy-Sn1YuCgEyTy_AMDuVgBgf1AHssXRymbE6A_IKys2ZxYZPYAUZflyffdUX9qmhtACaQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"ZVFUPkFyy18\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"q1VCGGAL3arQ5tG9vMefKaHC9EXJLlJYu7Lgk_8RBPKJ8yejiTkU7xRWwJowK9kLsyYTHvCsJlGc-phNyEAE58QoqmQGePbr80H_q-7fF3H85UsQ5XFg2A06KQYT3dLn57Qzsf-qlJKwrVR3Rrz1XoxYY-IgEHPad86xW5PlwPKiNY1ZaWsOjdeBccsgfCeG5tn13a3GY5BoX90w7b8ly_BsL904-_Yeog_deesQ39oE_XKpORBnDxvUjFtPJIpaPMCHEqAKiVXH_dZrcGUevs9xvDl2Odiku7pUam3atzNQENKwB9HMDjATVdYZfelF9cllDVAUXKFHMenwqoV7PQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"mROehy7CZO4\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"xq5YLUgQU82zwNtjw6xTbkAxpTp56_lnLy33srWPlZeFypbT8p2hC-918Vn08j-NuvzUeqarFLv7xBUZWrV2ilho_IYQWBZdMYCraBtDoLglctJtb6RzRG7rF0KsiyxsTwLCZ5UwcGpc_ZIrcDTRkmHvgqfA-KKpK_hIAwGC7rwNPDK0E26vuiaH85wnanQaWfdHzHFPW-cUWFbmjOZIQh0XHQSPnjE2JYX7rWWKga_8Oq7CUF_ArEF-8qTGL59GwS4OFcilvwyb53ANHy2bOEidRZCGQo6Kh2EjyjBHNB_YAiOLwfeTstb0fbDfWbfmdO3lW_-lBuGnQMPY5ukG5w\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"it1Z6EDEV5g\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"xZgfW761Mad1PHSOINv6kU13aiueEn19Ko3CR5EiuyuO5v8uJMfV24Mg8JTOxq9GuLIzte4CMg-5kFxQVopkqYZ8TP3eHAW6kWbh4j-C3I8vUJiA6LWGblFUsMg_sWvjwMPK4oF2bNeqGSVXSOtg3PMuBdV5wB4IBDikTovgSSbQ2gfgkEil94jOh0_bjzDXMDH6dv8Ong2Fj_bfWUg3MKcm6yVTCwCfqlfgNpcEqm6m3SVVpQRVxvlbsPmBt61w3QgOwC68rTD1BRVsH_DyL8DVsQKsg3PAbKqqNY0HM5YS9VPdFFxKYHiX7hZuNmtcPWDStxWSQNrvn4aaw5Xi5w\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts-acl/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7777.json b/test/resources/accounts-acl/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7777.json new file mode 100644 index 000000000..59a68b506 --- /dev/null +++ b/test/resources/accounts-acl/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7777.json @@ -0,0 +1 @@ +{"provider":{"url":"https://localhost:7777","configuration":{"issuer":"https://localhost:7777","authorization_endpoint":"https://localhost:7777/authorize","token_endpoint":"https://localhost:7777/token","userinfo_endpoint":"https://localhost:7777/userinfo","jwks_uri":"https://localhost:7777/jwks","registration_endpoint":"https://localhost:7777/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":"","claims_parameter_supported":false,"request_parameter_supported":false,"request_uri_parameter_supported":true,"require_request_uri_registration":false,"check_session_iframe":"https://localhost:7777/session","end_session_endpoint":"https://localhost:7777/logout"},"jwks":{"keys":[{"kid":"ohtdSKLCdYs","kty":"RSA","alg":"RS256","n":"sxljNM34KhyDZIXX6mjR0GIbs8Z_IzeBfoFDlkxhdf2Tigl_mCZEnc88fBp619e4l3D_t5GfyR0ZWQuhmCUTY8AJuKqdyuV_jU59nvut_izKydgNxBHGeFMd9abG-PTuq6iE3qEyr8A04KAsZZh2Zact5i6Xvb6N1GB4HDMU3LUAcUwkB6QhCpC4BPzwrTQ8DJZEz1O-_cZj9Y60gFvEo1NCLY6ZppYCfI5wqQhaQJ3jsG0TM03w4w2mcWALrIRoCrt-FIVqKHlKaeiioQALlj3Hdv38hljZtO7FykPqZE4N0nn7T1KQyj2LNCYDU_-ibTwdWm9yagdGuEWPCGvVnw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"gI5JLLhVFG8","kty":"RSA","alg":"RS384","n":"1LvKSpE6v2nhiIdErilavuIRu7aFc3Sej72jtDYAZze6R4dl3-nNnuaNBj8dD3PU16ZD8HQrLKTV6W77udl2yAjar-ZcVpItf7VUX_dCQsRehe7LVC_NgiBVFz88JI_rFF3F2WLC4rIXujv5XdG2v7UyV-KAODrPgY5-jfDJOv11_Klrrpekrtlk98STu71HJYTQR9CzQnMtxBVCOXOIVPVaexnV6gKBSrRtgKHqJxt8FRU3j6xYBwAdeDZaUyeHyUAz2oZkEHmNxoxEj-6yqaTf53AEf3EKbzYHCr4puRJx3H05ZLHkRoUG8utl7CxDsSQPDbwnk2jPufFSmPvR2Q","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"1ZHTLTyLbQs","kty":"RSA","alg":"RS512","n":"uXwK4QaRFmscFO4Sa5nKr5PwL2mWBL9e-omB2cCqqB2V6e7VHq5A_ybEFKXcXDGJKxxc2fHo_PNclUAqIr9Qa98nkQt0bd_F2QxtCqPc-3WcUoe3s3TIVNIOWwp93OAlabBkuNfb7dxnpUjYeGzIs-G7EPhON_5x0h2sC0r3v3Ev_J1mwrR3z9tpUzaODqmI2LKdc3Tu9Ha09CWzb4uRTXC4eVIJoEMxOelvxn6l8CMWLuv2XPaw-pMv33WK4QKfmnwJWO5TLvF2SaYR31oEL3GeG-SwIFTek1xX3cdeNljqsYCzHGHd4PxSqJGI3BPqn55FPCbdx46ZMmlOz_ImFw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"AVS5efNiEEM","kty":"RSA","alg":"RS256","n":"tQxTzwMoSCCRkiNUyp7CABfMZn8LU56axO31ErnW9qDZ4uuPdSO96nqHBU2JoMbnUjFQ9hufAt3UJPHDDD8kNoOOgEZb-CWnb_349oHb8bn7aIOpX1peukndSJ6Nt8SBvbARkb4ErI2b7V9588R8kPwVdW65BAK4ub1lc4EewKJWv4nVIvtp9m_qlohV321rru573hS3BI5qOX2NY1m_Abz4sBGqJVR1o95MqR2IYUeCSORPj34GSdHNUipMVJrouI7LAoO9dNhCu1q8Efy-Sn1YuCgEyTy_AMDuVgBgf1AHssXRymbE6A_IKys2ZxYZPYAUZflyffdUX9qmhtACaQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"ZVFUPkFyy18","kty":"RSA","alg":"RS384","n":"q1VCGGAL3arQ5tG9vMefKaHC9EXJLlJYu7Lgk_8RBPKJ8yejiTkU7xRWwJowK9kLsyYTHvCsJlGc-phNyEAE58QoqmQGePbr80H_q-7fF3H85UsQ5XFg2A06KQYT3dLn57Qzsf-qlJKwrVR3Rrz1XoxYY-IgEHPad86xW5PlwPKiNY1ZaWsOjdeBccsgfCeG5tn13a3GY5BoX90w7b8ly_BsL904-_Yeog_deesQ39oE_XKpORBnDxvUjFtPJIpaPMCHEqAKiVXH_dZrcGUevs9xvDl2Odiku7pUam3atzNQENKwB9HMDjATVdYZfelF9cllDVAUXKFHMenwqoV7PQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"mROehy7CZO4","kty":"RSA","alg":"RS512","n":"xq5YLUgQU82zwNtjw6xTbkAxpTp56_lnLy33srWPlZeFypbT8p2hC-918Vn08j-NuvzUeqarFLv7xBUZWrV2ilho_IYQWBZdMYCraBtDoLglctJtb6RzRG7rF0KsiyxsTwLCZ5UwcGpc_ZIrcDTRkmHvgqfA-KKpK_hIAwGC7rwNPDK0E26vuiaH85wnanQaWfdHzHFPW-cUWFbmjOZIQh0XHQSPnjE2JYX7rWWKga_8Oq7CUF_ArEF-8qTGL59GwS4OFcilvwyb53ANHy2bOEidRZCGQo6Kh2EjyjBHNB_YAiOLwfeTstb0fbDfWbfmdO3lW_-lBuGnQMPY5ukG5w","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"it1Z6EDEV5g","kty":"RSA","alg":"RS256","n":"xZgfW761Mad1PHSOINv6kU13aiueEn19Ko3CR5EiuyuO5v8uJMfV24Mg8JTOxq9GuLIzte4CMg-5kFxQVopkqYZ8TP3eHAW6kWbh4j-C3I8vUJiA6LWGblFUsMg_sWvjwMPK4oF2bNeqGSVXSOtg3PMuBdV5wB4IBDikTovgSSbQ2gfgkEil94jOh0_bjzDXMDH6dv8Ong2Fj_bfWUg3MKcm6yVTCwCfqlfgNpcEqm6m3SVVpQRVxvlbsPmBt61w3QgOwC68rTD1BRVsH_DyL8DVsQKsg3PAbKqqNY0HM5YS9VPdFFxKYHiX7hZuNmtcPWDStxWSQNrvn4aaw5Xi5w","e":"AQAB","key_ops":["verify"],"ext":true}]}},"defaults":{"authenticate":{"response_type":"id_token token","display":"page","scope":["openid"]}},"registration":{"client_id":"7f1be9aa47bb1372b3bc75e91da352b4","client_secret":"c48b13e77bd58f8d7208f34cde22e898","redirect_uris":["https://localhost:7777/api/oidc/rp/https%3A%2F%2Flocalhost%3A7777"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7777","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7777/goodbye"],"frontchannel_logout_session_required":false,"registration_access_token":"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3Nzc3Iiwic3ViIjoiN2YxYmU5YWE0N2JiMTM3MmIzYmM3NWU5MWRhMzUyYjQiLCJhdWQiOiI3ZjFiZTlhYTQ3YmIxMzcyYjNiYzc1ZTkxZGEzNTJiNCJ9.DNCfFeM-NyvWuZHQNJlVl8gFJaRh0vOZgoUX-88sGeFR0k9KS9poySBX8hNuZ3Lrnx-_A98dH1HbVijXHSC8pn4y1Lzmh-cnM-p8u5NWGxNuZt1uLHj8hdNJW7iY4cIFvCfKq3-eblDVbyTDfIJBGPq5x0kVZ2GC1M6Qo4mufNGiHncZ_QiZDW4l9VRM6mzZ0exoiHU00YwIUaa9rGepOefPuoEqOCE7RIxUrdc3Mwa_qgyDbJj3XO58r9JHMQYP9mcweTvLV9mth-B-Azo0kp4pC4TZSEb-5VPRnDgQME-boxDJIbsNP4LfgNSWqHhp5ZLuz2AzJJVsZH8-qbGPkA","registration_client_uri":"https://localhost:7777/register/7f1be9aa47bb1372b3bc75e91da352b4","client_id_issued_at":1491941281,"client_secret_expires_at":0}} \ No newline at end of file diff --git a/test/resources/accounts-acl/localhost/index.html b/test/resources/accounts-acl/localhost/index.html new file mode 100644 index 000000000..6101fdcb7 --- /dev/null +++ b/test/resources/accounts-acl/localhost/index.html @@ -0,0 +1,35 @@ + + + + + + Welcome to Solid + + + +
+

Welcome to Solid

+
+
+
+
+

+ If you have not already done so, please create an account. +

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + diff --git a/test/resources/accounts-acl/localhost/index.html.acl b/test/resources/accounts-acl/localhost/index.html.acl new file mode 100644 index 000000000..de9032975 --- /dev/null +++ b/test/resources/accounts-acl/localhost/index.html.acl @@ -0,0 +1,11 @@ +@prefix acl: . +@prefix foaf: . + +<#public> + a acl:Authorization; + + acl:agentClass foaf:Agent; # everyone + + acl:accessTo <./index.html>; + + acl:mode acl:Read. diff --git a/test/resources/acl/append-acl/abc.ttl b/test/resources/accounts-acl/tim.localhost/append-acl/abc.ttl similarity index 100% rename from test/resources/acl/append-acl/abc.ttl rename to test/resources/accounts-acl/tim.localhost/append-acl/abc.ttl diff --git a/test/resources/accounts-acl/tim.localhost/append-acl/abc.ttl.acl b/test/resources/accounts-acl/tim.localhost/append-acl/abc.ttl.acl new file mode 100644 index 000000000..27f8beee7 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/append-acl/abc.ttl.acl @@ -0,0 +1,8 @@ +<#Owner> a ; + <./abc.ttl>; + ; + , , . +<#AppendOnly> a ; + <./abc.ttl>; + ; + . diff --git a/test/resources/acl/append-acl/abc2.ttl b/test/resources/accounts-acl/tim.localhost/append-acl/abc2.ttl similarity index 100% rename from test/resources/acl/append-acl/abc2.ttl rename to test/resources/accounts-acl/tim.localhost/append-acl/abc2.ttl diff --git a/test/resources/accounts-acl/tim.localhost/append-acl/abc2.ttl.acl b/test/resources/accounts-acl/tim.localhost/append-acl/abc2.ttl.acl new file mode 100644 index 000000000..f4d4a027e --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/append-acl/abc2.ttl.acl @@ -0,0 +1,8 @@ +<#Owner> a ; + <./abc2.ttl>; + ; + , , . +<#Restricted> a ; + <./abc2.ttl>; + ; + , . diff --git a/test/resources/accounts-acl/tim.localhost/append-inherited/.acl b/test/resources/accounts-acl/tim.localhost/append-inherited/.acl new file mode 100644 index 000000000..7ef040005 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/append-inherited/.acl @@ -0,0 +1,13 @@ +@prefix acl: . + +<#authorization1> + a acl:Authorization; + + acl:agent + ; + acl:accessTo <./>; + acl:mode + acl:Read, acl:Write, acl:Control; + + acl:defaultForNew <./>. + diff --git a/test/resources/acl/write-acl/empty-acl/.acl b/test/resources/accounts-acl/tim.localhost/empty-acl/.acl similarity index 100% rename from test/resources/acl/write-acl/empty-acl/.acl rename to test/resources/accounts-acl/tim.localhost/empty-acl/.acl diff --git a/test/resources/acl/fake-account/.acl b/test/resources/accounts-acl/tim.localhost/fake-account/.acl similarity index 100% rename from test/resources/acl/fake-account/.acl rename to test/resources/accounts-acl/tim.localhost/fake-account/.acl diff --git a/test/resources/acl/fake-account/hello.html b/test/resources/accounts-acl/tim.localhost/fake-account/hello.html similarity index 100% rename from test/resources/acl/fake-account/hello.html rename to test/resources/accounts-acl/tim.localhost/fake-account/hello.html diff --git a/test/resources/acl/no-acl/test-file.html b/test/resources/accounts-acl/tim.localhost/no-acl/test-file.html similarity index 100% rename from test/resources/acl/no-acl/test-file.html rename to test/resources/accounts-acl/tim.localhost/no-acl/test-file.html diff --git a/test/resources/accounts-acl/tim.localhost/origin/.acl b/test/resources/accounts-acl/tim.localhost/origin/.acl new file mode 100644 index 000000000..632b670e6 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/origin/.acl @@ -0,0 +1,5 @@ +<#0> + a ; + <./> ; + ; + , . diff --git a/test/resources/accounts-acl/tim.localhost/owner-only/.acl b/test/resources/accounts-acl/tim.localhost/owner-only/.acl new file mode 100644 index 000000000..632b670e6 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/owner-only/.acl @@ -0,0 +1,5 @@ +<#0> + a ; + <./> ; + ; + , . diff --git a/test/resources/accounts-acl/tim.localhost/read-acl/.acl b/test/resources/accounts-acl/tim.localhost/read-acl/.acl new file mode 100644 index 000000000..3cf47cdbb --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/read-acl/.acl @@ -0,0 +1,10 @@ +<#Owner> + a ; + <./>; + ; + , , . +<#Public> + a ; + <./>; + ; + . diff --git a/test/resources/accounts-acl/tim.localhost/write-acl/.acl b/test/resources/accounts-acl/tim.localhost/write-acl/.acl new file mode 100644 index 000000000..632b670e6 --- /dev/null +++ b/test/resources/accounts-acl/tim.localhost/write-acl/.acl @@ -0,0 +1,5 @@ +<#0> + a ; + <./> ; + ; + , . diff --git a/test/resources/accounts-acl/tim.localhost/write-acl/empty-acl/.acl b/test/resources/accounts-acl/tim.localhost/write-acl/empty-acl/.acl new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_2d7c299a1aa8e8cadb6a0bb93b6e7873.json b/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_2d7c299a1aa8e8cadb6a0bb93b6e7873.json new file mode 100644 index 000000000..491a9fb87 --- /dev/null +++ b/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_2d7c299a1aa8e8cadb6a0bb93b6e7873.json @@ -0,0 +1 @@ +{"client_id":"2d7c299a1aa8e8cadb6a0bb93b6e7873","client_secret":"b2926a0f21cec49c906b7b7956cc44ce","redirect_uris":["https://localhost:7000/api/oidc/rp/https%3A%2F%2Flocalhost%3A7000"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7000","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7000/goodbye"],"frontchannel_logout_session_required":false} \ No newline at end of file diff --git a/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_370f34992ebf2d00bc6b4a2bd3dd77fd.json b/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_370f34992ebf2d00bc6b4a2bd3dd77fd.json new file mode 100644 index 000000000..d68fbeca4 --- /dev/null +++ b/test/resources/accounts-scenario/alice/db/oidc/op/clients/_key_370f34992ebf2d00bc6b4a2bd3dd77fd.json @@ -0,0 +1 @@ +{"client_id":"370f34992ebf2d00bc6b4a2bd3dd77fd","client_secret":"1fbd9aa5561f242e7f9b1f95910a722d","redirect_uris":["https://localhost:7001/api/oidc/rp/https%3A%2F%2Flocalhost%3A7000"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7000","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7001/goodbye"],"frontchannel_logout_session_required":false} \ No newline at end of file diff --git a/test/resources/accounts-scenario/alice/db/oidc/op/provider.json b/test/resources/accounts-scenario/alice/db/oidc/op/provider.json new file mode 100644 index 000000000..e6cdabd8b --- /dev/null +++ b/test/resources/accounts-scenario/alice/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:7000", + "authorization_endpoint": "https://localhost:7000/authorize", + "token_endpoint": "https://localhost:7000/token", + "userinfo_endpoint": "https://localhost:7000/userinfo", + "jwks_uri": "https://localhost:7000/jwks", + "registration_endpoint": "https://localhost:7000/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": "", + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": true, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:7000/session", + "end_session_endpoint": "https://localhost:7000/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "exSw1tC0jPw", + "kty": "RSA", + "alg": "RS256", + "n": "stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "XHWy74gIj2o", + "kty": "RSA", + "alg": "RS384", + "n": "rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "1pWK9Xv5qtw", + "kty": "RSA", + "alg": "RS512", + "n": "2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "hE56feUj3HU", + "kty": "RSA", + "alg": "RS256", + "n": "5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "TcucFsr7B9c", + "kty": "RSA", + "alg": "RS384", + "n": "yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "7cB0RYQoGVA", + "kty": "RSA", + "alg": "RS512", + "n": "s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "_KGGowgYPwQ", + "kty": "RSA", + "alg": "RS256", + "n": "pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "l_E5e128umo", + "kty": "RSA", + "alg": "RS256", + "n": "stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw", + "e": "AQAB", + "d": "SUhkMW-WGlRHIvbgEwJdPclNkfJLDMd_G11QbO0-sh7GOQFBsAGJDsSMQZLViFgpK07t6SqdKcj1O86FtFyVpkkREOYlx6k4g0Fq-AsKbaIPy4Cb7sTU4axFTs_5E6jddepPnX-ts0z67QRTq0YGKh5A51JPCiNGrR00R8FwbcEteWjsYxAf4KzwQxwrMZEITGmUffyaxznbRqtSU3ST969HPB6S1Z3BGbexAqxEsqTD1Vpb7m5XBOzn-DigjD1IqRbBaWM0IrKPzvbNyZ0LoePeS1B2k-h8C9IpXnn5xnIZM6CZsxuZeNDDmrONn3EmCJA0Fx9YN7LRNKcJFal2wQ", + "p": "1fz98Do9Ff9lB8FcdVEA26v421RqzYjiuW2QrTybYd2dJhSybpebFltnLa7H9YVNg0vd6h1y-q3iQkXes96GPqy-b_qdhfVQMOOUN0WIXUATMqXsSOOqeGeICVMgfH2rRpJybwLV4Q8izBJmdM_KoU8xO56vqV-OkkpSZIkn3jc", + "q": "1fVgIH6raoxkuA1ZzMy0AwFcPrqJOBNV1giZTOAe39d5Q1fyFl2nrveqIMqYiDPTyJcpsNavvBC5l_P92aZJIHxb3N40Ui_lYNSE14RvkUIw_YPMEnxbeYa2hqEMZXQHS6MNYLDILFz8Ap_QoB0Xb7JjEsVUirgMrLW2yT1zN7E", + "dp": "ByaTHcn0bJ3CNIYjns_8JVsTz9B8WS3v1Z5xrThPQO_05mbep49tYUvgoMgsamnv8yk_2yjsxK-21dwb2wrelY2UN426YdWWvmt8cnRiYCtZ-OFOigkBk1ByXU1n0oEojg0qwcboesLUuNkMj266KLXKwWFGIXTOANl282EZ8fU", + "dq": "Rn91Tv-tx4u-3A46GosQfTUDif-4mut0CvQGXxgx1BuRbykZMVlmmPYt7mQS4j4BeESmjggPG25_WJwidoad7cBMHHhy0OnLMJ6VrtWKVVhz__RfV2_2TBKhLbb--KbEiJ2PGN7m9gclWlACU9-CC2HB1zuB4btHIdk2AxTmU-E", + "qi": "qauM3eBMypEMnTKqoxjszHynwt3fKvUxg963o_dfTeqaoR97Ih5QWJKyULKE502vU2cTjDPrZgVd5O9B_A6HFZ0Yl3XFM-S29ecncihhc-DD1Dk0hOvUXW-mdwVAyPxfbJQaegfQXczLcGvjjTjTKO2SDf66hWYZ8jZB8-4aibo", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "exSw1tC0jPw", + "kty": "RSA", + "alg": "RS256", + "n": "stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "sATqsrT-GQQ", + "kty": "RSA", + "alg": "RS384", + "n": "rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ", + "e": "AQAB", + "d": "XZKYF_SirEW_XFyW58V8gcJhudUG_BXTilId_EoVEuJzCWCVoXMyr6JlO8diO1ic0YFXuteTsYk4FJ6pAO8kxO_dT3t1ni0Hy8Li2oiHpI_0tg3mzNhw_CWVYiB03GruNwVBq2ga4ycEi5CEl-MlSktwnYZvgwRDVsMaGpqmK_r929mR9jHDeBdOTi8-7h5tNHb0MrY9iAx9mkQj8HZxJWXfHVRNGcgC7IWnVQvxnRpDQYN3FPHS570-FnloZ1TB539JSwANR1umcRoHpsjd7Qdjh4e0A0SrXqPqm5kiRi4BWGoS9TatzaN7HVNbbX2dThUZ9U-1FGpNRp5emuy9QQ", + "p": "2u_7hOtsgJBnAKpKgeA5GtfAsi9LQ1c37SVyQlluPHHk9eB7P8rF4Zb35RIy6Kab3e6eP_gSuj_uNT8J8i7BWdYn1T7gxBbQ9Ey979LOYHO8AKai4xw9r1iT37eEdrKgd_HdGTQ44C0MXSUt17_NzkdTRxDhTcgHPdKWi422Kh0", + "q": "yjdtmCRLz8xLFu33hrWrUB95CdRuUmM8WjREnssGj4_NRMx_0ueH8xHQAZKJlTND8FSKvIuVdE85J1WhM7VQkqP5YMJaDBI3gZg0YS3sLTu-euFQ-IYKOs4TavUhNIRMz6QF9QugQWXIItwHnFYWw1Ns8Z1wjWCbWRjHzTxilw0", + "dp": "FqgLAUBTpCJNZnY466PGhQ6atFXMlhVqhjH_1vnmPH8U0JUAbCORwryavqvZdNX4_0h4O-pyFbAT-JKjdtp7y84rpReyrtglm4JtjWnlTXnslKyp4pLDl2e1NcuJ-7aUgJUY6kjLMfe3ddQpIFCK_bPH3GzUw_XVOgKW7a4mkck", + "dq": "fK1WFgLy9yjXd0i7X8Qs3ta40vW2G3fx4w_s6xb0cZlRD0Ui3o9AQ_7Mh9uolmQoVEpby8ooGLEr5POn03DMP8132U-bI2wr6uxEB1LAFleKpsq7GK_UKNOcJ0sB8RZNIYzY23ASm5-8mLmeu6ZcnIuYVRQkLBbPUUy1C_ZaNxU", + "qi": "MX1QubAxUhCMJ5XLcnWcTxVtO66pJjGWDGuhwjFXhZ7UCLRC5zTQtyt8vA_44wKVls1Hb0sAppP2CEqeVKkVbBnJGeglBPvBbFD8LblW3Ba0R5R7rnUGaffQs_N6Fg6IAtYWToo41U_g9g-OULxYy_KGTqyNeCEGD5bCexrvAJ4", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "XHWy74gIj2o", + "kty": "RSA", + "alg": "RS384", + "n": "rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "eXp_3Brz-R8", + "kty": "RSA", + "alg": "RS512", + "n": "2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ", + "e": "AQAB", + "d": "rNhhSgycUENnOiWdjGhyMVxh3_T_FFloJzRdXQ12XEmZlGjWO6RHtFMCk5HDpjwSkWeKqZ2CGLvW9HMzPS15m-58_A0_6O89wt32s4U--Fwwmlmd79xJkQksdWEA9wo4KAU_a9A7q1PXEaQE4UBdQ-7QBP_dYrzaBXWvJwnpl_gLWxkfiZs3jTkGEpIFd7g_kmsbeVpsYx6qRgXApEK_d2wqnSKkNOcqZOG7zQnS4ifcAajXJvJ1Y9LSVtvGYgVegnWsgJgokKhjntjIXFX_lWBWRs33xZ_4yRATLA9MTrkw-2zVJje03gJ0V9h3A-QtxkDTDXW7K222hveBvYH6_Q", + "p": "7CpHUXHOIXF-X7Kqi6-ystj3b50w93QZa0728AEU_u-C6J4zXm5vLujDTtPLQ5sxgbG3S1V41dAGetSi9Xff1uq8T8SZbhZq49xATKUkYrADz_XzNni3f8F5eX3Q2zNIbtAjvx8HM7h9ax6-MITx3ewOhl6jf7U1fxU7e8Uhdxs", + "q": "6zXLCILjwlxprXQSgssB6tWJoDHFdTWFBYEjA5-O8QuBNyKaiflkgN7nZ9-cmSQotUHTOLc7A5v-uTk1AvNLvsaQljGvM-HRahfWc3gW7lHX8i-3bDsj08xvQ8IV-SgvmcvcQhL32_ppecH7jT1Wglo4IGbUlsK9Vwked1rJJg8", + "dp": "40ScdUgrsgtiLf3mGZ7vPSWGmKaQ5NGZVKcdBEJGTj93nxv_GzTzUhU1PrqatWi377NyTNDoA_q5AaN3XvoJMu2aYrkzXbm9C6J9TkTuCvqP8KUjdJwfGpa5q6zkPM3ROrKac-YMLD2ylE91f4OwrnvoTm7ssI1V-gIYyDcgyVk", + "dq": "mJh_rnfsd64oyWVilQRLrCT5crqXlmEwec-7_Z_Ixs1l-XUzuYvZDlqO2q8SE7CH0IByHnuRh9fuvBBHOjDJ1W1RZH-7YPeCO0hX0vX4OolShkc6wrbjmYcqMFV8l_bgWvENZriToV2mjF2za4B93XfWrf7IsT6KRCsgXuLBWTU", + "qi": "NQYUSbdWwwxemxx2pvHF-dhZCiPBsepVeMwdbUNjC0UbFBbCTelwNtE3xm3NJkVrpSowUd9aFeXQCeNObHSYvzW1dWBsNIB-dkVwysuMU2ejEP6YtC-XwYE_TbdZreXXXUK2WIdgmrhSYFkrTKyyRss_cYNUWVk6BbgsDKsZzrE", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "1pWK9Xv5qtw", + "kty": "RSA", + "alg": "RS512", + "n": "2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "swAojIt6-8E", + "kty": "RSA", + "alg": "RS256", + "n": "5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ", + "e": "AQAB", + "d": "llIkJ9J0dJhgCyfZVnmc12KwoqKWOx0CKisZwhWKWGsEW2MuSXmIeQXrSHIv_qptkT3rxbjYUk2vffoQRwCAv1LB8-4bP4uOGENV2Bx5wCf_Kn6wYhE_bgT2SElpooCpJi0K36OP7I134z7s8Tiu7uTMPMjxHZZZ-ltov7rYJQKnAALoNu9gHKXl6hipaaGokwxuoXPGZ5uU8MxZDJGSb9mjDDqMyZHFiFOY3n4xP2JIOXpK5QkhZ5PNgJPkE5gRZ-XpYzG4pmvNV7c2ZCzzqE-ql3F0FlB3SXoPVd-r-bHFLS9aqxsUXX6vwI-_6aacnZQOCBMTuFbTXrB8rrNpdQ", + "p": "_2bxDC1YjTIYH20XVSsPZG3MJDHE1VpoT0QShULB2l_DzYAz_BYz8jTXYVgrtBsrrIG1bKbJCDCe9inv4MhaDrLZJdb0KDg-fnphkUi3va1O6cQp6Ap8tlhyqvkXWJzVjMlGX94-8t_6GnHque2fCYk_pVFFtaN2OM_t2JRqluc", + "q": "5cl-pGUP2Ngtvk7ax2bRUL6zMjC2q_K8zNMOmwRNTXVVm60O01Klityq0HKSn9tXWIBqbQUIj2Omx9Sbcoy7VzJWGxwDrxTzsL1QmwsxT-mCDlf2qU3P4ccLUiCCLax_j4UlF-YZRU9yYCNYAr8mHRkr85QJdcoeACjEu5XqFnc", + "dp": "DyvZQ8TpzrFcF3nOegOtzWRsTPYb4CSXr6W2h-34P_WSVwG3lNDo0wlqheDL783xYTTvRv39URw6RRsmoa6lEtxy47mKFV2J8M9qPkwYhg7mciJx7tO4pshIP3m-dkgSs2M9Z_J2wMynOazsqZDA8rsRacuTHYARRLytP0FJt58", + "dq": "IXzm_vBXieOfbv-w9KRwVtMj7GmbBZ4fk74if8b1uRcjfcePxI5j38PfBPfdlHxz00sLt42nPLZqJO9AJEaMOt30HIlNpCNqjFRave24pwvBz3NUWEIlzKKkbLieICfmgzUFPeFjx20Xnxknh2byGAWGGT52znrBOoa2fRwQ_Gs", + "qi": "hK7sHbYVIe55AJnufneLJCKg1bO6_XPZV-R92auZ6FbLQNM2S66dSzK1-meElikEEe8z1eBlas-WmF2fe8JBaARsv25ZEH02ii_a8BMpvarIycack5Xhp9vR0ka7MoMUSyy6e2WkEUDLPF1HdNeB6L9DzYd5_zjKC8GZFskFJmU", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "hE56feUj3HU", + "kty": "RSA", + "alg": "RS256", + "n": "5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "XPq2mVZnAjg", + "kty": "RSA", + "alg": "RS384", + "n": "yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ", + "e": "AQAB", + "d": "Nv2-wYgMXdiACOmg_t5hmKZwimnDNHuqlV0t75hvqexVb2O9AxKchJrBferfLEJ3_qwxtXe_JuRDKL_TPTuF2m5U9Iv0NUTGmH3btWWzRf86SFh6FZs3L5s8T0-TZM9butp5pQr6O7jcKLKmu1gusrwmdT69DJ0e8MboBjDAGmIbaFltdAlLlZDrLqXXpe2Zp8mJR5Z4ZzdsjC4x6bZv43T_NMfHOA6TZbSudcCBWLB7u71CNNoXoBfbgtkc3TjiDRjTJNgpdBNn9dEn8j21MyB38BE70aFxk9UrnBKMn7N_4biIiCv6Q1ybfB5S9LfJFIPQ21yRINy9QzvJYWLpBQ", + "p": "75bMx_iD5mEhQ-Lx5q6qzR5W0ABBUfa0ppbmCPwxhbeEYDClXS4lWtw9NLbHhg5D9LE7oqhGHhbjS9A7cpDG4R387sHCET-kcy5mPPPPUigQdS6daqBvu8q0fMclYMVlYZn1PBysQ214uiTkJoI-JXEWondiUp6xAyV1d6kNgdM", + "q": "1pcBXUZsJ4z9B_FHe-98d29qUPWiJSET8RfOmc2cjx40VLXLHGjGLr-UKUD2zf_xg-kNOW6YWTsLKtNb11bxI2-yQ5Ox2c-FmNgM9t0RPXImV6GSxx3KJ4cgcIo6FlCHiTrfXXwmytLXVmQ8pTtjvDsiG3w1Ezf597bq_qyqmwM", + "dp": "Ao4uMvfQmFVy4GF8SQSV58gqDt_h0nj6Jki3vWLLOGzjqY77RIooddahhH1qlWBzkxmM1EhNLyb5V6ap66flpyMFvposcrimDWByULYdAPhSbJ2Jqkh5yJv53tbU7DpOwYK93d1EbReu0PVxxYNgHFAfeK4jS1RL-QeeQB96eGc", + "dq": "rkoVnJmvDGyRsxrAEaRQtnzyn_DxkjCMjtvkPL1oNEG3BTpmTpu2o4-Mmfkeu-_uTFJEIGp4KLkw98aVKJB_6GU3J3XVFPBdNOf9l5-z-fE1vSUJHtpOL86rhVxvk2Iywz3i334Pz9pxdcSSES3scpygtiwqu4JSb2TM9q5tHts", + "qi": "2IfqemcBaYDFbjYW4UcRtMUkfKrqnvU-GzWTUMEiFvkBxxlP15D50ySdS0_Eic53EFVnEiBmhPuni4XnBRjiTI2bAu7LzMZl1VgEY2bSE76GOv6kV5Ecut4uI-XIGKXTSeNbT98amr84n2M3LC0C-sweC_X_3y_Noun3xRutMgE", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "TcucFsr7B9c", + "kty": "RSA", + "alg": "RS384", + "n": "yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "L0DYxpASjy8", + "kty": "RSA", + "alg": "RS512", + "n": "s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ", + "e": "AQAB", + "d": "N8vBkwYfhgXiyT4fZOYT1mcxzNSiNxytYyueLhgPGHbF_p-LtG_euLYycuihN_D4utibq6vu4SRO8ar4evSb5agCdGNCvMpXBQ2Mxr9br790VYFyEksVRUFBwAuRLF0EAqNXrXu5Sa-BbTluy5xtdZ5DIABxJSsFiZXfjgibTkX87-7IhFY0RqZwjJm0dtSnhOQNnBmSOEb76gTWne3KCrNSzsx1wgwJfuJbAdEwWCeW15YriwkDZmIb6afABNM8MLNkVQ4ZFwbvuaPsqBL0wfx8LzMln1VJ96MjCoiVRNCJY_ockRTSxs1pKnQODcxH0LpFAi7A3b_Kgx5sBFq0gQ", + "p": "32QJa7FOIoryyKpjhq2RVqxLxaooQtATJPY6c-XL8M_YOA8R2sbpIMaky4Q8HhXUgXW8uONhQ44czBYMcMmS8ScXD138oTbwSFDNWHuj0YaQ8CRl_zpddhcqpUagnfQap2GuIvBcD7KawuaWRszbePtusEgcYHkFKWzwKECmOZU", + "q": "zfsqq6M07tApFzL0DBmfOTvlbCAK0OR2JQfacPKmKC7dTk7c6576xXaxRPFViosRiGQto-6RITuvH4EUgvnCwaReh1e-DM_PVZDF9brQaAgbrdAJSvKcD_qmc1HpaWvGwRjVTTXs24RPCeAhT8pdMCFfiwpU1Kw7jdqvIinKxm0", + "dp": "tvtIRDBd4imSqQ_4qi6uKCLFhknU5LVvmQ0f4CNRJBX79B9T7rKT70cHYbUVUUdsZAa-6WtHFoDn0bwVwKU8edAdMXc5IgzQUUvuiBXuoAfr3OjTq3Zxa_OZ-PubQQbcdlKqwu_DWRBheFhMq_3NoJHDnx3SMKuwsLgNF8us3Ok", + "dq": "C0OxEbHbMzQvCxW-Qusjyf18jm0yKjpUO7IyP_sFGy107NNjQX9wN1xGVX7dLrZsPwk7dbuWNDsPWKm2dXMzM2PJx50Ex66VqBhCuy18ODQ5T0gROggKgNU0RRo1qY47UFQLVi2cxmR17hRTvglTD07D6talzPueRiOvcC7Y6AE", + "qi": "q3TgXAaclJJhu_-ZPexPbxPBcv859J6Ne6WnmvsMkgfWwo3GWwmQLTzNzI6Pqh9QzYL4PqKqcKjH_pnaGMCEKKmXGlTohZbe_0UZPGqBrlBgZMUZvd8kB6OWFYjoWg8C8RSdAeHTpmDsGa_1ofpYDYO2VnAZwBgWKkUzGw38a0s", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "7cB0RYQoGVA", + "kty": "RSA", + "alg": "RS512", + "n": "s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "zbOTdFBcKjs", + "kty": "RSA", + "alg": "RS256", + "n": "pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ", + "e": "AQAB", + "d": "IV2pMpPFxWmTIvBtQLxMCPvnn--TXyE8NCBfn5jgUqO_dPR2VGWl9rVF5NGUaxW6UglmSBqajf_uZjQGnVK33QE_qEaCPtP8SzyLqwKbN_T3frY6PLnuDaoiRLJS0R45XcVV5qWrxh091CyLgPWCFfPp_IlmucECwkgBApMNylFXppbH2n10Dax3_BOemsue4dSTPnuKuoo78d_35McDW6bmbDRTpFFmDxLRXJcsQDx8MDasWArb9qhV8_Viyzc4-XWcKh2D1BgrAfO7fSFiIeBX-kX3dEcKauXTKCQHwELympOydR9oSFv4rUtr7YLDQTIGSMisS1wVSzteTbv6AQ", + "p": "1O3G1u_5giTm3iXEpKcnZdPcqOE5SATcSnB0vcNTnPC8YjDqG2ag_fIOPBJaeFo08aXcM4RB-VEJoYaDcLSTMz34zofbziPRhVxwXOPmICROcUOFJkWVHVkLn-s6-vUAD0-sbjr2_V5GNQaanirBiDqF9qEwM6X_67BX4aJuqZk", + "q": "yFJh1X2MSsbBHyCf5KgXBt3U-F7GuNqlKg942B9exh2pfxtDZfI3iChuEAMS-a1qbgjjo-tgGg7H2cfDFNwpz4nwLunYNFnTdEItuDqiMXi4GHKGeE9I0jM84S0vuEqCUfl13YNvcTylzafDZYHHvw7qrgUyWd8LdVoJ01DZxfE", + "dp": "qhxEzRbvWWAt6bB2x6ybNyjpkypMXxMzA22QdsKEHE_f0PqPLdDyMa-eW7O1_4zh22TM5YN2Sb7KWPdkLzi0mS2bhzTXEHthOpA9XJjeEzOuT6LHz2mr1cR8GwkNF82AfLsEYRROmuEkadyazl4OO821lPH11m16Zkt-Ck-A5ZE", + "dq": "kgud0CwsMAgfnDYI3Ie_4f2w2zMd5n9hkvycudSFICNYA5c42AZzfg0b0QisuOM5iOdqL4PXGKhWA-yjyX2J7gk-1rUeL2ydwVDOTFZTEYZVkV1NtED5cmZwqCptdAq-YE1jJRBCG2h_6SO6TTMFEcIqTpzzTJpUnEX8i9eSLcE", + "qi": "aYSpgbUDpt2fs5OTNEPILU0dK3A3x3G9e3s8ENjKl9U_HLrPNwhVZy5DdoIRI4mMvLFUyYLAmOH44qVdi1Vj0kj4T2U14pY8kTrQ84_fKpuWXEqNHXN230xjD2MkRYZk8S9nutNrScZTgCEnqAmlaUxj8FfxnF6owD71QHjcALY", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "_KGGowgYPwQ", + "kty": "RSA", + "alg": "RS256", + "n": "pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"exSw1tC0jPw\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"XHWy74gIj2o\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"1pWK9Xv5qtw\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"hE56feUj3HU\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"TcucFsr7B9c\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"7cB0RYQoGVA\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"_KGGowgYPwQ\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts-scenario/alice/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json b/test/resources/accounts-scenario/alice/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json new file mode 100644 index 000000000..f0110b7cd --- /dev/null +++ b/test/resources/accounts-scenario/alice/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json @@ -0,0 +1 @@ +{"provider":{"url":"https://localhost:7000","configuration":{"issuer":"https://localhost:7000","authorization_endpoint":"https://localhost:7000/authorize","token_endpoint":"https://localhost:7000/token","userinfo_endpoint":"https://localhost:7000/userinfo","jwks_uri":"https://localhost:7000/jwks","registration_endpoint":"https://localhost:7000/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":"","claims_parameter_supported":false,"request_parameter_supported":false,"request_uri_parameter_supported":true,"require_request_uri_registration":false,"check_session_iframe":"https://localhost:7000/session","end_session_endpoint":"https://localhost:7000/logout"},"jwks":{"keys":[{"kid":"exSw1tC0jPw","kty":"RSA","alg":"RS256","n":"stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"XHWy74gIj2o","kty":"RSA","alg":"RS384","n":"rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"1pWK9Xv5qtw","kty":"RSA","alg":"RS512","n":"2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"hE56feUj3HU","kty":"RSA","alg":"RS256","n":"5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"TcucFsr7B9c","kty":"RSA","alg":"RS384","n":"yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"7cB0RYQoGVA","kty":"RSA","alg":"RS512","n":"s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"_KGGowgYPwQ","kty":"RSA","alg":"RS256","n":"pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ","e":"AQAB","key_ops":["verify"],"ext":true}]}},"defaults":{"authenticate":{"response_type":"id_token token","display":"page","scope":["openid"]}},"registration":{"client_id":"2d7c299a1aa8e8cadb6a0bb93b6e7873","client_secret":"b2926a0f21cec49c906b7b7956cc44ce","redirect_uris":["https://localhost:7000/api/oidc/rp/https%3A%2F%2Flocalhost%3A7000"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7000","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7000/goodbye"],"frontchannel_logout_session_required":false,"registration_access_token":"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MDAwIiwic3ViIjoiMmQ3YzI5OWExYWE4ZThjYWRiNmEwYmI5M2I2ZTc4NzMiLCJhdWQiOiIyZDdjMjk5YTFhYThlOGNhZGI2YTBiYjkzYjZlNzg3MyJ9.FL7GfVjf1faSrKg6G7EmQyGFpprHf-Djw06kLypEu9__g2ozzSgxPzo2cgHWGc5gNQ9D5FU-unwZmx354WvIk0DvU4GF_sDhG5gfVgRUiwNgKzgyaxl87aoUG4jYfwHDYwvZLXCPIuoCD7iB2u4cD_NYhK2u6OQST9bRSTlelrXN0MyJbDy1eItY6ys8yH0Yw-584SK6ksZh2NmjvBr73znmVI0xHdv80ntcrfagw-G1PK79OG_DH_wjPqoUI9yUxpY2AjnLkqbraQIwT6Uwx0eFNCj7OwVVoIOkxdDMCargpSHF1jvBBL8wsXqppuEy0YhHYIfU6POFZBofRJrKtQ","registration_client_uri":"https://localhost:7000/register/2d7c299a1aa8e8cadb6a0bb93b6e7873","client_id_issued_at":1489773557,"client_secret_expires_at":0}} \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/db/oidc/op/clients/_key_eafafc5103e5b15ba06c3bed7c5dc3df.json b/test/resources/accounts-scenario/bob/db/oidc/op/clients/_key_eafafc5103e5b15ba06c3bed7c5dc3df.json new file mode 100644 index 000000000..614f15ff6 --- /dev/null +++ b/test/resources/accounts-scenario/bob/db/oidc/op/clients/_key_eafafc5103e5b15ba06c3bed7c5dc3df.json @@ -0,0 +1 @@ +{"client_id":"eafafc5103e5b15ba06c3bed7c5dc3df","client_secret":"5eac22a963328151a139206a35036b17","redirect_uris":["https://localhost:7001/api/oidc/rp/https%3A%2F%2Flocalhost%3A7001"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7001","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7001/goodbye"],"frontchannel_logout_session_required":false} \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/db/oidc/op/provider.json b/test/resources/accounts-scenario/bob/db/oidc/op/provider.json new file mode 100644 index 000000000..4f61349e0 --- /dev/null +++ b/test/resources/accounts-scenario/bob/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:7001", + "authorization_endpoint": "https://localhost:7001/authorize", + "token_endpoint": "https://localhost:7001/token", + "userinfo_endpoint": "https://localhost:7001/userinfo", + "jwks_uri": "https://localhost:7001/jwks", + "registration_endpoint": "https://localhost:7001/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": "", + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": true, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:7001/session", + "end_session_endpoint": "https://localhost:7001/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "ysNKuDh7-rk", + "kty": "RSA", + "alg": "RS256", + "n": "wvMeFsXkedSC_tnFgzvSHSYqoki9d95_l6Rm3hcwNknOkaycrrJketqeE4oSq_H4curUdPjUXYwu5e5LSoEZERLNElTXY10MUpu_he0DrhlsnWbBlzm6e3YuPr3MZlO_beQhpVtTnPTTeOZgOnUK9A44uqIzWoh7uaiU5uRi5JrZFtVpk2KGp49o68IXkSvhd0BkFaEBB4r-BSjpWwXKeu9Y1Tp2V7C5pKpXHZwOzI4LZru-QoTARlLKGsFPxTjK1E47N76dy1usoKLu6Xs0toaiXnxNUTLPk4ERg1kk93mvHkiIDsP-jVawJh-bhWLXQEEm7lbAV0IkcySqiJaKkw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "Y8dNW6a_V18", + "kty": "RSA", + "alg": "RS384", + "n": "xQNISCAVvlsB4VTHq9HQcDf3PxF7D9DvnTNYPtXAxTIXx5bXVX4WxJU2xSTkYtN0k-yAMXQed9MAYNKsNwD7NAO7RV7m6jCSIgD1FEu3V6iEeliMetL4CfIe_Vn7Rb37lSI-gKaNMwBVIcYoAy7xOXLxxpSFJ5t357HbJnd3p0cgvx13sfyz-WyxqMLWY5IdxktwS-tdxUmpsk6M2xbcJB97c4h4afrfxp68ZB4fznC23aos6QUm7DLhGOURJAdwQTebUre9J6Vy3BXfKNpXb62AGpzPLGDzt-c-kQ05ckEzo9ZZZVC6l-DfMryb5rLZKlMKTefzL12ricSRcltcZw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "BSILu2VUSq8", + "kty": "RSA", + "alg": "RS512", + "n": "2OyR9CUp2B3_XrC1rwx3CxvsGenGyyjj5i_BMUyi8biEAu7N3aZ7AxvaSVtYGeWCWDRmPE2XImoEDtLBdG3wlOroOlRvgGnd3hlajqswIRgy3dmmbVETNqqJJQefc5tRESsA3VHKz04H3trcibo-ycM5HRc3cGXdWExg2XQUxkmOXKVCUEBnMpeWGlAG-QUGjGP3DVZ0V6-ldQXH_lP1ftt5zTWusOp0iyrLbvX7eWduVlfGsIHYNi3cVJdAxbZXUMwOwyHn3HUrlCDi1tc8_x8-pq2SgQhTrJQVF3D8UExYV_k6cTQOXRqJgz7LcISYyWULm8FM2NYWGl12MCMqqQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "xuMN0hE4aNA", + "kty": "RSA", + "alg": "RS256", + "n": "xs-BOX2tAPab_6ftuKFJNqJJPAMf6NnGEt_KPEuQKlS6Eoqxd1Sl3V6y8mj7g4TTg7Yb0JT0GjUmKs61cJww6w4JIQepzAKb_LT-mrOjckWTDC4lUSYm8IX-tfFDUKhkYh-rOQz7rNQ13BKQ_MHKGY3_imzp5tRvevkbwHzGjHRVMPKzRFBm20O5_IOSCFLYp0dIi-zKK7gSpZFfMW6ZoAoZiOhBoRhNFs-XJ6UUcAifNmpxnCDM9KJBGv7YCVroYnyt7pz0xSrab72ZGPQQo5EqnjvckO1ACQuekJfOCQ0c2yVd48y-W_wTDvSn1ZKOdecTE0BbQg2P-h1HYN3RFw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "hrVDwDlmtBc", + "kty": "RSA", + "alg": "RS384", + "n": "na2HnmI9weG040vd5v8mC9RkfzKmil-GtZxUNtCndW3MV_55x5yBund_TSo_rDHrlKm_ZvVWhvkhHtteZ-V_Yv521zA_vVaFVwCGQ0-KXSRW6GtereabW835tb23nQWItRepT1SX4Z_7tpS-_anpVVwaKvUqEJcUptFfkGICP98yMnemGkAR-ejLVNSElh4u9FU6q8Y4wBuBv_VRtcFanUcsnSDWIjCL0YyKZ1Ow7FqvGjpglBHsfzeWFyX2Hn2JZvozWNMGGm77ietL7fsPfvfAilrHXXFNk0Oso8DtQnj6Ft1oXLUyZijSiTN7AubpdaylW7tjbkXf42ZmPadjvw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "5DeLhvjbXpU", + "kty": "RSA", + "alg": "RS512", + "n": "xH5VCmySFeekK1oYflMd6XWV9PsNP8JBUbwhR0Uq4ANRPVdhzFc1N8GInEl-XWgBU9CYtLhMB4CrgRiFgSQPU7AUmYfmmaZ4ScGQItpIHcL5TSELw5ncQTmv4TYTEksvvESm-ihRbN6Irhrm-_izjzXZd1yRlpZJL-e4L5CGlIl4s1_ZwhHoF79Nw0_ql4Awn4hJQiZzdJnaJ36ltSVfIN750Glyv9MGVATpwKSsEtIiDHw8szcLXv04wPdmwTcblhgrSrgbPTn4YHpjmq6I6iFJz3sJEAGT-XbB7PdEC3Snk9CC8iJzaF-DrRVbp2BIi4Vo51AC1NPgESDU8lSWmQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "5lqnxcDvwtY", + "kty": "RSA", + "alg": "RS256", + "n": "nSn6UV7vgCImW0PExOhWUOqtT4_SM1ZShwN-Ti-4sIfiRgaOw1_Wf4PAHkqQmTp8xiOZhDOfe2NTDGhP0VENkwILPs_kdHq-Pm-4Qq4tx9nSEKdjq1XlEP99wmtmMQOSBdenwzkKzkXMMSROOqs3iItablA2vFnVfjZUsEioDikn6sQIg7nwQT6Sf76w1wv5uYrVlc-nU6FPh_08-h5C_IL2QNpbRBHM1BKtZEH2njDnSKVNFzwuwDfnjRtKwOtAmOwxxO0xXZHlDZYYE4tAlbAX1anJj_mjWxoLDPwQKvZCMw_XPLY3jo5nsSGOX2bBCWsZsZcbs_Cg0t58DldC2w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "kUaKSoYRlpE", + "kty": "RSA", + "alg": "RS256", + "n": "wvMeFsXkedSC_tnFgzvSHSYqoki9d95_l6Rm3hcwNknOkaycrrJketqeE4oSq_H4curUdPjUXYwu5e5LSoEZERLNElTXY10MUpu_he0DrhlsnWbBlzm6e3YuPr3MZlO_beQhpVtTnPTTeOZgOnUK9A44uqIzWoh7uaiU5uRi5JrZFtVpk2KGp49o68IXkSvhd0BkFaEBB4r-BSjpWwXKeu9Y1Tp2V7C5pKpXHZwOzI4LZru-QoTARlLKGsFPxTjK1E47N76dy1usoKLu6Xs0toaiXnxNUTLPk4ERg1kk93mvHkiIDsP-jVawJh-bhWLXQEEm7lbAV0IkcySqiJaKkw", + "e": "AQAB", + "d": "FYM-jsTHsaoByp29bf3r8cuEaOQeBFxJODKsJ1XnvBXo9apWn_CPpf758q4J4W-SOxwu1bmftbMCed0R8ebNHVU1zBAQtKZP9c26FM1S2qNzmOr98fIs4fLop4PKSoBzX20NSXIKiAd8Tpsdg1XnH4dyOyYrBJKLFLHFvLT3NMKwPrcirpyWueY_pvnltfTJ6Pog-QBiD7c0_FigHDt_azQnmrpMBpXFjkSR8Yk1fW3X45BnYo4RrB5KSyhBxvQfh4VwF0-7Ynv0_WoNbGxvnpYaD1lxBcOT3l14kRyiqEnTR_SGZ-vi-HSgJq0ifXNg9GpbrTTXe6b2Llbu0ymEAQ", + "p": "8UB6QUoYmOef7ojDR0mhewKqbi9XXU5NfOW35cixwg3X3igHEwnpGiofJCZvGBIiXPAlz7WVw4Rp29F6AoAOZwhg9cTfgPk6ACP8pjHC961LSoLcinA4sbW8VR3gKVgAqgIbA8GvxvWWS9I8NlqDSEK-BJ4hKDZHhW6JNAlpfNs", + "q": "zt4GJvM54aBTY0OBw0ISVYsZAa68OK0Es2d0iaE1mlAUjEYNxLBDy4M_TDxxz_red3mwuuftI3n7ZOPOxe9vQfftkXUkrkzGOS6GLwBuwAtLdj-_dwNc5AcQ82J04oc-Ri4GfsNel7jmCL3lVhEtxjgDsnhnRNrN0BrMRW50uqk", + "dp": "uRxOMjaWdQyU7MRHgjV_EBHVj8IHePKSBlmFJ20857cTgcSY2QTrtUXIq0ZKS9_uOf2SJbQg--poB2DOC4kShAAr1aiADkgtNtpmC2d3P-_aK4wJiLfe6IyXu3-29kIuEESZUeKV60WZUwg3Z0VAInwDrStgKaisbDeKU0E9ja0", + "dq": "rXzCCBRfbHt6s3q_7rMQkTEwbZrPO3DOym5u66WJQLr8II_3qAZzNNADW7otcNDhla02q-kplWENlhT_KjydP-PfFuf5NTwp2XbNDcn9F43hYXAg8HyfgJT0gEkH4ZqufUjIJbNPN0rXkGlBVibeDqiXYStc3__oLyjqOyhhONE", + "qi": "Jkf4EIZQdQRDW2AWGU18a1aQcBQLlEVkbsuneBlJLbGOOIQ88RiVY-ozvzrJvYM9veTWkVkEauZQktJ0cpddQjMjwYtNx8Bb5Gx53W60mrxu60_8TlKJBegGfRb95sdTZzhSq5Ww6ug82MaTbjW5oVP-b_j2RjXPtloQTQ9d2SM", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "ysNKuDh7-rk", + "kty": "RSA", + "alg": "RS256", + "n": "wvMeFsXkedSC_tnFgzvSHSYqoki9d95_l6Rm3hcwNknOkaycrrJketqeE4oSq_H4curUdPjUXYwu5e5LSoEZERLNElTXY10MUpu_he0DrhlsnWbBlzm6e3YuPr3MZlO_beQhpVtTnPTTeOZgOnUK9A44uqIzWoh7uaiU5uRi5JrZFtVpk2KGp49o68IXkSvhd0BkFaEBB4r-BSjpWwXKeu9Y1Tp2V7C5pKpXHZwOzI4LZru-QoTARlLKGsFPxTjK1E47N76dy1usoKLu6Xs0toaiXnxNUTLPk4ERg1kk93mvHkiIDsP-jVawJh-bhWLXQEEm7lbAV0IkcySqiJaKkw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "oVhE0V7u_5A", + "kty": "RSA", + "alg": "RS384", + "n": "xQNISCAVvlsB4VTHq9HQcDf3PxF7D9DvnTNYPtXAxTIXx5bXVX4WxJU2xSTkYtN0k-yAMXQed9MAYNKsNwD7NAO7RV7m6jCSIgD1FEu3V6iEeliMetL4CfIe_Vn7Rb37lSI-gKaNMwBVIcYoAy7xOXLxxpSFJ5t357HbJnd3p0cgvx13sfyz-WyxqMLWY5IdxktwS-tdxUmpsk6M2xbcJB97c4h4afrfxp68ZB4fznC23aos6QUm7DLhGOURJAdwQTebUre9J6Vy3BXfKNpXb62AGpzPLGDzt-c-kQ05ckEzo9ZZZVC6l-DfMryb5rLZKlMKTefzL12ricSRcltcZw", + "e": "AQAB", + "d": "hgTzmMzqvbFfGA_6PHHgX1ZTBT44_stdFQ0mjKgQGJU5A_ciyO-bQWNX_MhZ7Lh352DM29doGo8ZBMSLjmdGe13GeSxT3R_paORJ0-Kl-CWU6T1vYuY9AVcJqcMaYZkZmZvP9OhXyUgCoZURBwlZ44DD5BiIdCcYbC7bK9G9Pba7ka2lagn_3ViBoEb73UE0UtMpphkDtAxua6YOxEMOd19sGl50g-ftuo9UaKUdfC0DFQTlXU9MCDGG-JqNgppBZUOlfhZIUT9mh4T6gNesVye59nk9RAVyuUBHtXK6rDwcySGO5_FIwZFtFL8W5ea9TwVv-svIkgmaHWs57muZkQ", + "p": "-7AhdR1VEC-6c0Uvp-WDp3Y7Ai_5I_Xc_DZovqI8svA8_NLbu2f9JzDiMAhDv8-QSoLCm2_Kn4imeMoIoNra_ZBzp6Ln1Jw9M3aK2evZhRduKcU3x91gzKndhbmWYSO3fw5oQS4IoINBvVML9grlx6ab1rxemlJg7RU_gkilXEM", + "q": "yGNajiog3smLemQH4sYmVzxTSjaHZgfhnwZLSpLtALS9sKHU3CPbMHrVdseXaItLUyh0FwCL0dKSPCBowUphfqYIcNt9Izip0E1fABsTvcKDP_jhZSzgTUhWuzWCB7WGp_pmQCCWV2oE259UhYkgZIaJWYjmzL4PA83Nv4I_Tw0", + "dp": "iZAe-U_q6knr8oziGzZK2wC4B94IoisDeaaTYX5zBqpf6x-kka2oo_8H4ZDi1rev-cm2bBaR_NhHhMWIKcL05ppJXFqhs4chvDsScUGDRkckIxh0AH1zJunA9hIVq0pGRN-vA9ERTgnvqHb3lqcmKBVcH-YdHuPfrjVq3N6v4tk", + "dq": "VID5bhw78leB1yIZ5Tr0bjNFWHV4UcGfFsW7uH4PLg4KNFN6hT8lruMN4-I1amPbZv0XP5_-VoR7IJn2MxTf2l3AD3-v3MuHaQ1Hs663e31shey5eEYdbNnFoXrmE8QsPegteHuFiuVtmQQuy4VRQLMvdq9xzQOVJ2CBlHIjqn0", + "qi": "mfrXhy1lFdgQCraMerWD3xcU3hlqVEaG_1CrJ2oiPK1y0pJQ9WrI52BTZj3vyRXAK4FOIzD-3u8BRQI8d-8nT0k_0X24LuskWx-90RDz8_9I95cnRARzqmWJpXq0wXfPgPSZ9C9Q8HHwCq-liRrgcO9MRiU4zaulUe4m1fWzbUY", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "Y8dNW6a_V18", + "kty": "RSA", + "alg": "RS384", + "n": "xQNISCAVvlsB4VTHq9HQcDf3PxF7D9DvnTNYPtXAxTIXx5bXVX4WxJU2xSTkYtN0k-yAMXQed9MAYNKsNwD7NAO7RV7m6jCSIgD1FEu3V6iEeliMetL4CfIe_Vn7Rb37lSI-gKaNMwBVIcYoAy7xOXLxxpSFJ5t357HbJnd3p0cgvx13sfyz-WyxqMLWY5IdxktwS-tdxUmpsk6M2xbcJB97c4h4afrfxp68ZB4fznC23aos6QUm7DLhGOURJAdwQTebUre9J6Vy3BXfKNpXb62AGpzPLGDzt-c-kQ05ckEzo9ZZZVC6l-DfMryb5rLZKlMKTefzL12ricSRcltcZw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "HvUsi1wyEco", + "kty": "RSA", + "alg": "RS512", + "n": "2OyR9CUp2B3_XrC1rwx3CxvsGenGyyjj5i_BMUyi8biEAu7N3aZ7AxvaSVtYGeWCWDRmPE2XImoEDtLBdG3wlOroOlRvgGnd3hlajqswIRgy3dmmbVETNqqJJQefc5tRESsA3VHKz04H3trcibo-ycM5HRc3cGXdWExg2XQUxkmOXKVCUEBnMpeWGlAG-QUGjGP3DVZ0V6-ldQXH_lP1ftt5zTWusOp0iyrLbvX7eWduVlfGsIHYNi3cVJdAxbZXUMwOwyHn3HUrlCDi1tc8_x8-pq2SgQhTrJQVF3D8UExYV_k6cTQOXRqJgz7LcISYyWULm8FM2NYWGl12MCMqqQ", + "e": "AQAB", + "d": "qTsmQoobsvb0GzNRCld3J7uI3k57QFOOOC5ZUdSv6lRA6OjUwm722N4J1eDlQo7CuX1npPQDTF3Y_Jd_RQ3f-s2ojpkAw_XTmUm-VD9JNQhj5p--LtppWuSASdtAkjhBbltWOVNuHXyKt1mXY_tgsJcVH5TVM2LE4_XyOLNO4cFhFBWopGMAmlnBFpG0jhjQcuO6ksQ1TtH4MFiDl8msgbea8b0kjjFFuYctxVJ-yT2EnzfvXP_2H5S2yKJbma-Z5EKOqLhnsoM876bYUxfkzc-mP-fD3S4PXzpvasntlTOU71_lxhcXaQ4ZHnnooHnbbAY5tzzvc80E1mXhbenAAQ", + "p": "7TUW9r9cN-SsV8A6HlTn76YbEpbL-YW2GJeOJlTuqw2J2WBO-JciJWgtv_k9LQBAZCl2-Qm3fPQqhQSUVZ7SYOoqmNKB7qIT-a5-LWlXN-p1tWQ5SSL4eVk61XK9iP_ziB_yUX0H9WbouKRUwALczWoL0Mnp6SiWLjEc78-M2QE", + "q": "6hwbJk2omJXtq3a2PRSvYegQ_ItgVxMmaxYF-NKgl7wA2Yn7Tw8jauv3cL1hn5axSOt7v5Ep1crfDudrKfrkgWCFFhGYj5_85YkBpG8kK9Og51qogEzJu0Fk86ufvamqrYPo14oUKAWrNztZXZyrTMPXdWUOHOe-zRiqRkSm6ak", + "dp": "fm1XafggPKIiwTpxP41deTt9HnFFEh8UKRNN7lxCQOUcXcGZFaHnzywxhipfUsbZiwkWojFtnKm-p9sC_IeD9aeZQI6iNgAoyWEZWzbUB7dtOVrLtZFwAa1vUCixoH1a3Wi5jHkpbsCEtTTQ_u4HpWwqFAQqKd05_jCrDZ3_ogE", + "dq": "1vaB03UBd0JL3uJ9Sa7Br8PYPRx5lNrHrxKk3yoAPfNqUFXLhXegDOCo70Nl7ZUAKrXXhjpz0JScpuHF2-E9irKm4XG8xTyhid54vJU1AG0tVOJA0LYxkhjk6n3PiubNCtCRr8Bg67Lw2SFM2JEwFafKIkhtYgtFfqvERgtpvCk", + "qi": "Oll6LNvGj3NAwPZevSZJLuj7tSkqkUA-bosX_igVgXq0OKKre-4NgJdlztu65rvutOT9spvwxCpvOw3ZK_dDSf0ByW-rvUeqsuITDHL8FlYLv6LaPcLDC73Wm5ZxUUC5Ek5psphZ_6gPFsEe6h5oNtiqCyuT3BCRb7MDxnvFXb8", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "BSILu2VUSq8", + "kty": "RSA", + "alg": "RS512", + "n": "2OyR9CUp2B3_XrC1rwx3CxvsGenGyyjj5i_BMUyi8biEAu7N3aZ7AxvaSVtYGeWCWDRmPE2XImoEDtLBdG3wlOroOlRvgGnd3hlajqswIRgy3dmmbVETNqqJJQefc5tRESsA3VHKz04H3trcibo-ycM5HRc3cGXdWExg2XQUxkmOXKVCUEBnMpeWGlAG-QUGjGP3DVZ0V6-ldQXH_lP1ftt5zTWusOp0iyrLbvX7eWduVlfGsIHYNi3cVJdAxbZXUMwOwyHn3HUrlCDi1tc8_x8-pq2SgQhTrJQVF3D8UExYV_k6cTQOXRqJgz7LcISYyWULm8FM2NYWGl12MCMqqQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "NUV7ZrSPLDA", + "kty": "RSA", + "alg": "RS256", + "n": "xs-BOX2tAPab_6ftuKFJNqJJPAMf6NnGEt_KPEuQKlS6Eoqxd1Sl3V6y8mj7g4TTg7Yb0JT0GjUmKs61cJww6w4JIQepzAKb_LT-mrOjckWTDC4lUSYm8IX-tfFDUKhkYh-rOQz7rNQ13BKQ_MHKGY3_imzp5tRvevkbwHzGjHRVMPKzRFBm20O5_IOSCFLYp0dIi-zKK7gSpZFfMW6ZoAoZiOhBoRhNFs-XJ6UUcAifNmpxnCDM9KJBGv7YCVroYnyt7pz0xSrab72ZGPQQo5EqnjvckO1ACQuekJfOCQ0c2yVd48y-W_wTDvSn1ZKOdecTE0BbQg2P-h1HYN3RFw", + "e": "AQAB", + "d": "iNmdYi2YQOmASGMXx9d9xhW_w4eDF42QQr25P_fjrL_VtZ2yuymRYugk1aheORHdZvScAOAHh2K-ewj-7B0XrzViI1JbFwtUxIbyGxs8jxRFPEUnesyAUWBrDGKeq46-Sqzx97twIm-RA4PkOZhLvXt5Q5flCfeBJW0EJWO-aD8VLzAlgh3blAQu5DbZbu3-Lj52cmcDa8rdCMo7_Hiv_-W8urlOWxtr-n_Y_rQo49ZzZnDDJumjbi5Fe7X6NPLqfCDe41zkDmZVf2z43MR25vHoE2nPF5LdqXO7_qobkv5X8IDp5BmVRygc6SJ7ikmiYPA5Rtb9ktMb836qKkPFAQ", + "p": "4aquWL8PbD0lpcygS5N-tCjq-BECXq6d79IqmRhDOvUtmdhhM_lgsztGeKF-rfolJDMnqdQZa8VxBeliiIc1dbZ8hJElX7qC_yZNBRij2UZEfo-ubeolXNwnxiL2vOGhZf_UwTWcslO74KoZNogROn1h9a3qFzCzX3LtimReGBE", + "q": "4YiwMWCB9O6ntZ7ahtfvn_PTGDoaAmvn68hUFo0W9e4-tszd_A-flzDcxKTa82vzPOz9_kLNtfGT0K21m_E011Mu0FC79H7FQKDRLwaji-Bb-hDnCK3xTmGW3Vzu9R1JPY85CW0Fi-ofCHwDgKpn4OrXIvwxg4YoZQ_GSqsoPqc", + "dp": "E5X0u88ZT5OfCNzRrL2IaaqDejQ_uGf_XSkoeVEZxKwy4P9esFwcgHHMk_uwOvlS7-lgr-SwsCHaxWCUJLVXdnf4JqlSTRSq-eohFSgmUF1A5Jsj0HZZ981DxnaSY6JRl8C0fnBgwTlzPPSGa60zkZgAQIpvnsOjTc1zwGclo4E", + "dq": "PTGPTPZ4jHKswpTFikzQ0b-giTRKllmc5dbHKg9CKZxpG8RefuPmU2mInTp1xhKGPwO2ruSFWFah2r8nRZae1cXWL-OX-_DhqHV6DJ5qhatsiV9IsIwxqyjDfHCYzZ0SoEdaHHqeRKZToUO015Zk9RwDH5T6AkvGbhVnoh7qnoU", + "qi": "fJecRKbQTygX6BNDCcY4w1bWuftHr34gFEqivXKmligqvWwbg9XWvmRiW8-1sVPmFRV-IY_t65GcXqgict3j6lUWtZAvENqBcKkGvyoT24TiLgXNjfplvKjQNL8KrEBxKRy-Oxki0GSeT2NlLBPMkfGvqvPv5DruEYcKM2AyJjA", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "xuMN0hE4aNA", + "kty": "RSA", + "alg": "RS256", + "n": "xs-BOX2tAPab_6ftuKFJNqJJPAMf6NnGEt_KPEuQKlS6Eoqxd1Sl3V6y8mj7g4TTg7Yb0JT0GjUmKs61cJww6w4JIQepzAKb_LT-mrOjckWTDC4lUSYm8IX-tfFDUKhkYh-rOQz7rNQ13BKQ_MHKGY3_imzp5tRvevkbwHzGjHRVMPKzRFBm20O5_IOSCFLYp0dIi-zKK7gSpZFfMW6ZoAoZiOhBoRhNFs-XJ6UUcAifNmpxnCDM9KJBGv7YCVroYnyt7pz0xSrab72ZGPQQo5EqnjvckO1ACQuekJfOCQ0c2yVd48y-W_wTDvSn1ZKOdecTE0BbQg2P-h1HYN3RFw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "OcLqXHXRY68", + "kty": "RSA", + "alg": "RS384", + "n": "na2HnmI9weG040vd5v8mC9RkfzKmil-GtZxUNtCndW3MV_55x5yBund_TSo_rDHrlKm_ZvVWhvkhHtteZ-V_Yv521zA_vVaFVwCGQ0-KXSRW6GtereabW835tb23nQWItRepT1SX4Z_7tpS-_anpVVwaKvUqEJcUptFfkGICP98yMnemGkAR-ejLVNSElh4u9FU6q8Y4wBuBv_VRtcFanUcsnSDWIjCL0YyKZ1Ow7FqvGjpglBHsfzeWFyX2Hn2JZvozWNMGGm77ietL7fsPfvfAilrHXXFNk0Oso8DtQnj6Ft1oXLUyZijSiTN7AubpdaylW7tjbkXf42ZmPadjvw", + "e": "AQAB", + "d": "jTsaR149bMTK7fBNYRNWs6_ZGCl5DGYF1fOhZru70q23I3X3BDkF5cwVBTv7bjQEi9MDcqf7icfpx6a7x5nDYRsFvTclUGUH5-a7W90J1OWwxlstodv1eeRjb0rwBMApM_NunnTp2Zkfr-lJrRGcwu1NLv8LX3LDd6v_yeZPl37ebvV5ravHJkCzcZkONN6V6Upwups6SPF3bESB-TmIw1jldx82xVZ1APILidvi2ekiDyDf29LoFb634ZFhbZ-rdFcA-xNKYiFDR4aEBBi932zFbJhSCV8L4xwN5OAgyuyTUOn_qWWC_QbSGRdBUMAoFsSdGLuUCeJpV5oLDYlVkQ", + "p": "z5Hv0Thzf20tbBImiJg0lw_D4wYJZrcZaLVzF8QZ1F2PzgK8_zmf50m5-2juyDSIuXLoZYrkmYSOdyH3Qvz8-SGrnkOtgWOnjgu_40vlzW0477mV8bOKr5NlcfTw3HGHXql_o022gOLb3M8UMk7HdWlF6iWtQ1_skKxCnafVmbc", + "q": "wneN4ub7gdnbvK6rxQ9ZV7utOz5T5ub4lJDITJfXCvdGmxY96EEjPBSxq2Xb6mOkf_FDQNc2j7IX0ejCyx8FM4PJRXZCC--wN1rzSbXbdNcgD07o9YbwC3LtRCfLIn7sWvffeYBF9wWY-jXrM8DAISLGY02jEOLczudMuzPxJjk", + "dp": "mgJb_85008078H2fHaZhDtxhqWZnP1EHh0tqM-4KhClPc7lQZcZpwIBRgBqhYOaps39wszbU2psh4X7QKWHwiSDUZz8r018PiTNqkslTnpI1tpjqikV-1zr0ABOPSuDpYfE9hPs6OHMaUsFK6PDOyWzstQhzgBQCQG2vl65ZrA0", + "dq": "Ndy3R-mCL-0Pl6spmGMv88TfrlENHB9NKpkPYWeNAFSNEdePPg0MnU9-BmMoDjubDHTek88IJbTGNDWr_maRIjuWO88NbBDvVeWzDO954VrUXmkUzSyawBEM9puu_9b30Bpno1eMCWdbf7H_e04f6Q2gtVCDoeG0FvqpnhA88sE", + "qi": "CxjFZyMEpkkQl-bcdWplO4nUcThslZaW2bbaymE-lqBwAFnPKabvuNAoKa8ebHzZXteMW5XzXNa3ySxu4HRz91BySL3aFgxIH2gl1SN3JRiVLNrVKQ3y89z6uk2xjZkEAYfgqk94xdqHKMXwdQg2-AFgk5L6pnGMu1IuDuYqxvQ", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "hrVDwDlmtBc", + "kty": "RSA", + "alg": "RS384", + "n": "na2HnmI9weG040vd5v8mC9RkfzKmil-GtZxUNtCndW3MV_55x5yBund_TSo_rDHrlKm_ZvVWhvkhHtteZ-V_Yv521zA_vVaFVwCGQ0-KXSRW6GtereabW835tb23nQWItRepT1SX4Z_7tpS-_anpVVwaKvUqEJcUptFfkGICP98yMnemGkAR-ejLVNSElh4u9FU6q8Y4wBuBv_VRtcFanUcsnSDWIjCL0YyKZ1Ow7FqvGjpglBHsfzeWFyX2Hn2JZvozWNMGGm77ietL7fsPfvfAilrHXXFNk0Oso8DtQnj6Ft1oXLUyZijSiTN7AubpdaylW7tjbkXf42ZmPadjvw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "QTJyqgRbytw", + "kty": "RSA", + "alg": "RS512", + "n": "xH5VCmySFeekK1oYflMd6XWV9PsNP8JBUbwhR0Uq4ANRPVdhzFc1N8GInEl-XWgBU9CYtLhMB4CrgRiFgSQPU7AUmYfmmaZ4ScGQItpIHcL5TSELw5ncQTmv4TYTEksvvESm-ihRbN6Irhrm-_izjzXZd1yRlpZJL-e4L5CGlIl4s1_ZwhHoF79Nw0_ql4Awn4hJQiZzdJnaJ36ltSVfIN750Glyv9MGVATpwKSsEtIiDHw8szcLXv04wPdmwTcblhgrSrgbPTn4YHpjmq6I6iFJz3sJEAGT-XbB7PdEC3Snk9CC8iJzaF-DrRVbp2BIi4Vo51AC1NPgESDU8lSWmQ", + "e": "AQAB", + "d": "cSMohgsDhldNIKUMq1fiRjUtNdSDyW1pPM2s_6Nkz305fq9coVKpUsQ9i3eJqfCjqwXVl2DybfN5TKi43iXMKWyeP9SPQ3MlmZo5CshSc8h60R0w79wchPXZPjxreMIP50BEQI-MarorSsw0qWqGwPPJlj-XmHRKCapLVB_MTaNii9_2bZHRYjjP6t-PCFXnu5ipCsTkeOk9e1YoShHbQlyU-2_MTgceta5DzzvRWxGgK24JM5qbpO_IIfwu4QkgW0FyAzIWhICu4Q3ubesUwJJj8v6lEyXjo7CzUlkpNIXbiA_6YevYR91tOxYgE6sUgqlja3F_30SYL54nrK7BcQ", + "p": "9ofgupNClmZ1qjLgw8O6N04i8sgAzKOS7tSgFf1Dsv_RkvZ71PcqLy3i7G0RwXHm1MtqXQAqLqI7gYzx0ILVd_P6tb4MLzULhRzCQxiYoyUJEpSjBtbqWlHwSrZJ9kO_1yQ4uu-ce80HzeWmzOyQoVEsj77UhTi4KBALiW5BdhU", + "q": "zApw01bVrRoYPhiDp22ooAx1m9Cm-e41JouihHR-WaOdTE_BwHO4FFRtkYeY6OLF5izmLfo3oLwOF1YTfRDyqU8bA9s3wkc9IOnZ0hIfikEIhvmW-2t_Sez3LXE_gV5zIu6HGJX-5LOLLkhO2Oi368pSL4j8zR8kGdSMuKIh43U", + "dp": "LS-CdTAAiGiHMIbaw4bgXrqnlTArVVa126iFHwKooepZk0IyODqFNNiIOyVSl840rNQLzrf1A08g8QHQYJNaZP4G-cC3ov9p-R_oSzv63gwvuYQczWge1Ccoj8kRjV2lj91HuJuqZtaRk5-ADxdc-vRR4pbrhO98cXtfYfUfcnE", + "dq": "NPiI7fTfKD9cB9LpavAHFPXnGnqCvuPenJEnsedkXfUiAwu5qzLfmTeJ8nwXcG5fHjCN2WXaRzpLFjfce12JAfdtdgTVZvSDpCXRzL2zvnq_sfrd_Yuc0h5Y1U1PRVC1512xaOqX79vEyFExVxKjnO07hOe1abMp9iK-HbjJv3k", + "qi": "4dWo4q0NpTJb1RQbXdj7dX1WBAQyX0RnYo2y9CSbL2EZBCev3CdP_YA7lxqAVXYbDE-gJsz5egukm_SMeMQH8yMUvk6E0-WRmq-WHu__9UkX3gyvwCISZD9u_cTauSYlgcQUIRFNGIJlmAobmKTIimRl7uieeg8QVRfJHx05Yak", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "5DeLhvjbXpU", + "kty": "RSA", + "alg": "RS512", + "n": "xH5VCmySFeekK1oYflMd6XWV9PsNP8JBUbwhR0Uq4ANRPVdhzFc1N8GInEl-XWgBU9CYtLhMB4CrgRiFgSQPU7AUmYfmmaZ4ScGQItpIHcL5TSELw5ncQTmv4TYTEksvvESm-ihRbN6Irhrm-_izjzXZd1yRlpZJL-e4L5CGlIl4s1_ZwhHoF79Nw0_ql4Awn4hJQiZzdJnaJ36ltSVfIN750Glyv9MGVATpwKSsEtIiDHw8szcLXv04wPdmwTcblhgrSrgbPTn4YHpjmq6I6iFJz3sJEAGT-XbB7PdEC3Snk9CC8iJzaF-DrRVbp2BIi4Vo51AC1NPgESDU8lSWmQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "0POi8t9HXMo", + "kty": "RSA", + "alg": "RS256", + "n": "nSn6UV7vgCImW0PExOhWUOqtT4_SM1ZShwN-Ti-4sIfiRgaOw1_Wf4PAHkqQmTp8xiOZhDOfe2NTDGhP0VENkwILPs_kdHq-Pm-4Qq4tx9nSEKdjq1XlEP99wmtmMQOSBdenwzkKzkXMMSROOqs3iItablA2vFnVfjZUsEioDikn6sQIg7nwQT6Sf76w1wv5uYrVlc-nU6FPh_08-h5C_IL2QNpbRBHM1BKtZEH2njDnSKVNFzwuwDfnjRtKwOtAmOwxxO0xXZHlDZYYE4tAlbAX1anJj_mjWxoLDPwQKvZCMw_XPLY3jo5nsSGOX2bBCWsZsZcbs_Cg0t58DldC2w", + "e": "AQAB", + "d": "mV9h3Q7cgxrQe7gCynZB5e1e_InKBDd1ijSqifqgLgYtl1DG-XsJhJ86WVVDD4W4RlRveEg4lt0zKqSRYB_NM22HM-EFfXZbOesk0k3Qd3vmOEJiTc4hIRlzzMuqiqKFWhY-rZF5LhuHTV20yiRUqXf05Dp7cAvrAKRcuTvuZQHVZPrArYHmhgaVHPgPq5-qbDkFEjePaccGNexI_GqsTHNsg5TNlf7ikwYjmwLuJbQH1Mg1LEB18mPOnIgTcroPtqXvRWem0KftPmuVCrK2yLHUYqbNC66bxkM-aeqapQ1RqI9xjYv58l0ttEwMkwBNj6XjSbTB_TpmtH2xIoPlwQ", + "p": "ygEbbSRFPIQWp92GQi64Mn6KdvuYIMsvb-2ALy0JeDWLM-gNdDBtWqNFdNizBQR46_-Us_pZk1E7v1hc5tGBcLFVY_NHsiGa92vMG5MsyS5CHlm7fAHSKQJuEJHdcHt4O7fto9V5PBmKGrBR36ds8N57ZybiuIuxgZXFrCcfnxM", + "q": "xyyBBWWPzwGEvF0Yob_6aaFtFEAWoeP0OBevQLaaZq4OSGUfpJJiNMv6O7oLUAfl0KD7KuQm0j5qpR9f0D0LbvieQebA18o2Hbg7rsJFc4gfIcxC6AUBW54lwWWlEOJ8zDGCtOmPyw_9p-x8fF4qpPx5fg8yDxdq3UcaKqMBnhk", + "dp": "Bwlw1iV8T_Zd_60E30tXWVL1Kd3r18CcP27rlzkfalObLMy5o0GIna6wXbiqy9LzD22Q1ZA0DKC4zxqZ6eSEeNOEoP25kqf_CP11V8SRu9Rjs0D2-gPqOUl_Yg5iw2dZseLfYWSvW3ucRv-7amofrmhhrh85qKodHeGEyFF4lYc", + "dq": "OZT5PBkvqVY0DM0RaPn6qH097uPUZztjCLB4P0pLezII-Q8bRdX4RHFQR-IykRGndFiGJNFPE-tto41dgvOTEaMZBc5zpC9W0-LGhnCt6YfKEFhgY3nG-bjQC4iaXzZLhDEwK6N2qetWlyy8lKwYwhgn-7Ti8RABGjYLL5Zuykk", + "qi": "FinlULLwiGlYpUK1ihss-CUfT3idKSAJM44vqj4IIwK9fuzTwwwGxleiRQrTbQAAEiv7bcCa9VBP7yMyyWdq3xu7B0nhzURZ9J5017pNf06a-cadhpqenLupLGBLwI81zFoiq5kmtClLUNf-PEAx_KvQyt54_3dfJrq-_xcmNWw", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "5lqnxcDvwtY", + "kty": "RSA", + "alg": "RS256", + "n": "nSn6UV7vgCImW0PExOhWUOqtT4_SM1ZShwN-Ti-4sIfiRgaOw1_Wf4PAHkqQmTp8xiOZhDOfe2NTDGhP0VENkwILPs_kdHq-Pm-4Qq4tx9nSEKdjq1XlEP99wmtmMQOSBdenwzkKzkXMMSROOqs3iItablA2vFnVfjZUsEioDikn6sQIg7nwQT6Sf76w1wv5uYrVlc-nU6FPh_08-h5C_IL2QNpbRBHM1BKtZEH2njDnSKVNFzwuwDfnjRtKwOtAmOwxxO0xXZHlDZYYE4tAlbAX1anJj_mjWxoLDPwQKvZCMw_XPLY3jo5nsSGOX2bBCWsZsZcbs_Cg0t58DldC2w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"ysNKuDh7-rk\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"wvMeFsXkedSC_tnFgzvSHSYqoki9d95_l6Rm3hcwNknOkaycrrJketqeE4oSq_H4curUdPjUXYwu5e5LSoEZERLNElTXY10MUpu_he0DrhlsnWbBlzm6e3YuPr3MZlO_beQhpVtTnPTTeOZgOnUK9A44uqIzWoh7uaiU5uRi5JrZFtVpk2KGp49o68IXkSvhd0BkFaEBB4r-BSjpWwXKeu9Y1Tp2V7C5pKpXHZwOzI4LZru-QoTARlLKGsFPxTjK1E47N76dy1usoKLu6Xs0toaiXnxNUTLPk4ERg1kk93mvHkiIDsP-jVawJh-bhWLXQEEm7lbAV0IkcySqiJaKkw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"Y8dNW6a_V18\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"xQNISCAVvlsB4VTHq9HQcDf3PxF7D9DvnTNYPtXAxTIXx5bXVX4WxJU2xSTkYtN0k-yAMXQed9MAYNKsNwD7NAO7RV7m6jCSIgD1FEu3V6iEeliMetL4CfIe_Vn7Rb37lSI-gKaNMwBVIcYoAy7xOXLxxpSFJ5t357HbJnd3p0cgvx13sfyz-WyxqMLWY5IdxktwS-tdxUmpsk6M2xbcJB97c4h4afrfxp68ZB4fznC23aos6QUm7DLhGOURJAdwQTebUre9J6Vy3BXfKNpXb62AGpzPLGDzt-c-kQ05ckEzo9ZZZVC6l-DfMryb5rLZKlMKTefzL12ricSRcltcZw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"BSILu2VUSq8\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"2OyR9CUp2B3_XrC1rwx3CxvsGenGyyjj5i_BMUyi8biEAu7N3aZ7AxvaSVtYGeWCWDRmPE2XImoEDtLBdG3wlOroOlRvgGnd3hlajqswIRgy3dmmbVETNqqJJQefc5tRESsA3VHKz04H3trcibo-ycM5HRc3cGXdWExg2XQUxkmOXKVCUEBnMpeWGlAG-QUGjGP3DVZ0V6-ldQXH_lP1ftt5zTWusOp0iyrLbvX7eWduVlfGsIHYNi3cVJdAxbZXUMwOwyHn3HUrlCDi1tc8_x8-pq2SgQhTrJQVF3D8UExYV_k6cTQOXRqJgz7LcISYyWULm8FM2NYWGl12MCMqqQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"xuMN0hE4aNA\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"xs-BOX2tAPab_6ftuKFJNqJJPAMf6NnGEt_KPEuQKlS6Eoqxd1Sl3V6y8mj7g4TTg7Yb0JT0GjUmKs61cJww6w4JIQepzAKb_LT-mrOjckWTDC4lUSYm8IX-tfFDUKhkYh-rOQz7rNQ13BKQ_MHKGY3_imzp5tRvevkbwHzGjHRVMPKzRFBm20O5_IOSCFLYp0dIi-zKK7gSpZFfMW6ZoAoZiOhBoRhNFs-XJ6UUcAifNmpxnCDM9KJBGv7YCVroYnyt7pz0xSrab72ZGPQQo5EqnjvckO1ACQuekJfOCQ0c2yVd48y-W_wTDvSn1ZKOdecTE0BbQg2P-h1HYN3RFw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"hrVDwDlmtBc\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"na2HnmI9weG040vd5v8mC9RkfzKmil-GtZxUNtCndW3MV_55x5yBund_TSo_rDHrlKm_ZvVWhvkhHtteZ-V_Yv521zA_vVaFVwCGQ0-KXSRW6GtereabW835tb23nQWItRepT1SX4Z_7tpS-_anpVVwaKvUqEJcUptFfkGICP98yMnemGkAR-ejLVNSElh4u9FU6q8Y4wBuBv_VRtcFanUcsnSDWIjCL0YyKZ1Ow7FqvGjpglBHsfzeWFyX2Hn2JZvozWNMGGm77ietL7fsPfvfAilrHXXFNk0Oso8DtQnj6Ft1oXLUyZijSiTN7AubpdaylW7tjbkXf42ZmPadjvw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"5DeLhvjbXpU\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"xH5VCmySFeekK1oYflMd6XWV9PsNP8JBUbwhR0Uq4ANRPVdhzFc1N8GInEl-XWgBU9CYtLhMB4CrgRiFgSQPU7AUmYfmmaZ4ScGQItpIHcL5TSELw5ncQTmv4TYTEksvvESm-ihRbN6Irhrm-_izjzXZd1yRlpZJL-e4L5CGlIl4s1_ZwhHoF79Nw0_ql4Awn4hJQiZzdJnaJ36ltSVfIN750Glyv9MGVATpwKSsEtIiDHw8szcLXv04wPdmwTcblhgrSrgbPTn4YHpjmq6I6iFJz3sJEAGT-XbB7PdEC3Snk9CC8iJzaF-DrRVbp2BIi4Vo51AC1NPgESDU8lSWmQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"5lqnxcDvwtY\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"nSn6UV7vgCImW0PExOhWUOqtT4_SM1ZShwN-Ti-4sIfiRgaOw1_Wf4PAHkqQmTp8xiOZhDOfe2NTDGhP0VENkwILPs_kdHq-Pm-4Qq4tx9nSEKdjq1XlEP99wmtmMQOSBdenwzkKzkXMMSROOqs3iItablA2vFnVfjZUsEioDikn6sQIg7nwQT6Sf76w1wv5uYrVlc-nU6FPh_08-h5C_IL2QNpbRBHM1BKtZEH2njDnSKVNFzwuwDfnjRtKwOtAmOwxxO0xXZHlDZYYE4tAlbAX1anJj_mjWxoLDPwQKvZCMw_XPLY3jo5nsSGOX2bBCWsZsZcbs_Cg0t58DldC2w\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json b/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json new file mode 100644 index 000000000..eb53eeafd --- /dev/null +++ b/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7000.json @@ -0,0 +1 @@ +{"provider":{"url":"https://localhost:7000","configuration":{"issuer":"https://localhost:7000","authorization_endpoint":"https://localhost:7000/authorize","token_endpoint":"https://localhost:7000/token","userinfo_endpoint":"https://localhost:7000/userinfo","jwks_uri":"https://localhost:7000/jwks","registration_endpoint":"https://localhost:7000/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":"","claims_parameter_supported":false,"request_parameter_supported":false,"request_uri_parameter_supported":true,"require_request_uri_registration":false,"check_session_iframe":"https://localhost:7000/session","end_session_endpoint":"https://localhost:7000/logout"},"jwks":{"keys":[{"kid":"exSw1tC0jPw","kty":"RSA","alg":"RS256","n":"stiawfAYMau0L6VtUt2DCt9ytp0JnpjBlf8oujcPJsZ7IGNl4cq9VDEkm6WKxiaQ5aHwjrIF4EtW97Q1LwUIloiLgYvgBj6ADV1Zfa7-KDIoSE1nH1Uz8NWbPwaJ4dsjDQUa8EOGPAHjw1zgmCnOd70lIvqM8MnNjg9haut3tUhrILOmo3ubExawkvtp7GdiUqwSGo5K7s1WcKP4nQgd8SNxVMBFAyWC380_ZXcPL9SKgDsw9DIExmMVDjmaPn4orF3zivqVfU0VHi7z6ObNnBia2U6FK-M-j1-nPVNXW2En2xrtJ-nnGoAzasQ__GkC0XpYLyjv_4kuGkEFUwN1Bw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"XHWy74gIj2o","kty":"RSA","alg":"RS384","n":"rPDDwDbxtk6wV4cVi5jhTDMyP6MisKZypSm6-JQ1sMGjY2TcwVAMugIsDdY6hpcWvfGR8uJymCmnNvHrYOKsMqCEmexXoGBg-gqsuitjzxQUQfmulcD5MGrbsuGVpmuPKQ9lkT0BjdTplKtrKvBqIrdWCIp5wivh0NxI3tqb7eEzMc1rJQ781SKlQAxM5BLghLoZpdUiyHl1sKYH5ofs7Qqn-MBagFMtmy8Fl0YrnX2CSKM6xwGOlqm6dbVGpLiOdBLzfL-9ICyg1zurxWOUSIKosBY_dNUdx3e9QdsbHD74kKCEYe-BEvgj8t_dnEST_8g4hmxEeevOdSuAkDE-eQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"1pWK9Xv5qtw","kty":"RSA","alg":"RS512","n":"2Pxvhef0LSwCNFjBnBnTeRnN_kc1G_frzLCTqyPMow8jICVmK_-44QlOi860J12rnSGYi-UWOtg5ZRTnNCAakMnXtqajjPQ4PxmcMkrkdCfhyShYMjmqTICGUPfOujX3d_oc3l-SSpBeQdpSejecaoyIAmR4Ra7x37PWiZgw2b3Ss-TMeL8iufc6221gNDAzmOlQmVby0SXz43Jf1WbUnRLBygAGmcD18CSawNSQL2lZMRtaFlTikZ5Nz9dbzUS5U8btg99u9cOL1wL6xLnMX2MdYImF_ThtDdFW-Q3_Xj8xYJIUinMKSyPofk0yOD5F0OcjR2IIp828BO42htb8lQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"hE56feUj3HU","kty":"RSA","alg":"RS256","n":"5UAby9SJp2vDnV8ZIq7E5HHtGYKbAVwTmzYSxbdcMBhJScoY2HX2-N8cqZNIf6RhE7ipimVkbYeXXX795DtnbCN9Jcl8iKbWBLDe6ozHyQ-ZEuzdWe8gSi6HGwCW3ECfN8dXUbS72BIvID1KAe2LoQQuyRx1A9nlHQCJao31w7-y17h-j13_X5YhmVBYmLwmQI-3yOI4AYGFgwEuuS347X6bDk4IoSSLVieM65SAL9djs_ZzIyXrV5BEf7eY-zCazRt7vdqn11W_aM-JdyS5xDrsgwVPhaksU50vgPOjfzbOLVALvEDQ-sxCuT1Ic6S3I9zrVq6SzORW7vZtiKn_YQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"TcucFsr7B9c","kty":"RSA","alg":"RS384","n":"yNViaNYveaDftUVYRQmi1JbzBBk-uvOeQ-4vr_levpAMCVFrtEWN5A6jWmhbD4B3nvAn9828cjt1697nNPOIbF2hzRWCZIfsN5YJUbhseREb05ZL5TLlv5TkHj3sdhpmQqcd6JWcCQDIbaiZeLdQ-Ljm5dbckZlsJc1eJ96mlXVlQ3VaLbrEJThXjJ_YtPfMq1vUAzHpq-OF4yhGoTzvcVEswiH0tyTDobmaQuGJq1DabTC0-Vt4TpmlxOHLgCU5-ofehHaIeLqwRUrl6n5gKo0CX-7a8qvGYNX14X0Iq_1CjhP1Q8619wcFfXESFgitl7EQrncfCx8TrtdOIuFGeQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"7cB0RYQoGVA","kty":"RSA","alg":"RS512","n":"s75H3KlbQgGFLGf_oqYC-cv0-iQ6iRi4bDs1x00taHeTJQPazYJny-plYxi3OU7U9kCChS3v2zIIiAb5IOHI9nxTtfra2p1-VNIEe8YLqpJsYbie5uXSGXNahHIsZNjYO0kdTg-WkUZOR2jyeSUPOggp2zNBM_9UUUhLWWVKE_SHshm4vbHJIxIZfDmwLhZEUwvgwO1-b-VAitNd4kQXfbg2KSxXPb7_pRK9qV2KJJJ4k4K2oa7tFfilXwB1FDZnPgPLxI7dmzwgwekngXJ5PfQrVvsUDBe9mZUH2wanZ5q3W9qF7yLQYbMi8l9O8CQYHLstSNNMDc4okYZQY-HCcQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"_KGGowgYPwQ","kty":"RSA","alg":"RS256","n":"pp5I4Ubud6110-hIvfFsosJLSn-OrrW1C2ck5751GydxikI6sQnMlqbAS1yjyZSWRYPKWR8vD5NRp-EKP2Hd0dS1hA9_hNeQ4JKCcvmlOpmy07ckpr4fg6G-l501-36u2pnH5lJJGvA84xlaEfcqH3urHhsPbrZaurCOhiBPON6ek2GF_H1sYvdzflQ0E0k5ibwHNdVE85Ou8Uvzw58eDl0uhlwpRPg_k_zQFyeNK8MyDTcnExR13xU4IcnQPz3VdjC6BnOZWDE_GmspCE_4apd3bSFEHcV9C4v1PCLqQurBXTs0vgvfWML9UnSqWoGlnkczpYGgtujnnsxRpWFmCQ","e":"AQAB","key_ops":["verify"],"ext":true}]}},"defaults":{"authenticate":{"response_type":"id_token token","display":"page","scope":["openid"]}},"registration":{"client_id":"370f34992ebf2d00bc6b4a2bd3dd77fd","client_secret":"1fbd9aa5561f242e7f9b1f95910a722d","redirect_uris":["https://localhost:7001/api/oidc/rp/https%3A%2F%2Flocalhost%3A7000"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7000","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7001/goodbye"],"frontchannel_logout_session_required":false,"registration_access_token":"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MDAwIiwic3ViIjoiMzcwZjM0OTkyZWJmMmQwMGJjNmI0YTJiZDNkZDc3ZmQiLCJhdWQiOiIzNzBmMzQ5OTJlYmYyZDAwYmM2YjRhMmJkM2RkNzdmZCJ9.HkMGXV33JfSf5VFF3eY2XdEnH1KkG911-1MSoAcHaUjkXRJCW1KB1l0ofMEqBeHb3mC4iC8xZ3TP4F5kz2fPDQvAZe3v-LNjO2N8vCEpnT3HhKQnRitsA2zx0V6_aiGCDSyTavXK27OmSYwNs50RZQeSBjy76hjsS_sHu7_W42UDVn-beMkKpOhHnHddrir75JcmkUh1YqYMgopClQkt-Y22kdAQ3of2l17_QVDSUxatUEUVDSj76p8MAkYxb2YTdwULb-9fhQoYsy9JJphf59Bn5L26MlFlL9OgBYZRwVE8zvlGdyxllcgs4nSQbziOuQmArfQV3L0r-m8zDZYykw","registration_client_uri":"https://localhost:7000/register/370f34992ebf2d00bc6b4a2bd3dd77fd","client_id_issued_at":1489773628,"client_secret_expires_at":0}} \ No newline at end of file diff --git a/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7001.json b/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7001.json new file mode 100644 index 000000000..f786e230e --- /dev/null +++ b/test/resources/accounts-scenario/bob/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A7001.json @@ -0,0 +1 @@ +{"provider":{"url":"https://localhost:7001","configuration":{"issuer":"https://localhost:7001","authorization_endpoint":"https://localhost:7001/authorize","token_endpoint":"https://localhost:7001/token","userinfo_endpoint":"https://localhost:7001/userinfo","jwks_uri":"https://localhost:7001/jwks","registration_endpoint":"https://localhost:7001/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":"","claims_parameter_supported":false,"request_parameter_supported":false,"request_uri_parameter_supported":true,"require_request_uri_registration":false,"check_session_iframe":"https://localhost:7001/session","end_session_endpoint":"https://localhost:7001/logout"},"jwks":{"keys":[{"kid":"ysNKuDh7-rk","kty":"RSA","alg":"RS256","n":"wvMeFsXkedSC_tnFgzvSHSYqoki9d95_l6Rm3hcwNknOkaycrrJketqeE4oSq_H4curUdPjUXYwu5e5LSoEZERLNElTXY10MUpu_he0DrhlsnWbBlzm6e3YuPr3MZlO_beQhpVtTnPTTeOZgOnUK9A44uqIzWoh7uaiU5uRi5JrZFtVpk2KGp49o68IXkSvhd0BkFaEBB4r-BSjpWwXKeu9Y1Tp2V7C5pKpXHZwOzI4LZru-QoTARlLKGsFPxTjK1E47N76dy1usoKLu6Xs0toaiXnxNUTLPk4ERg1kk93mvHkiIDsP-jVawJh-bhWLXQEEm7lbAV0IkcySqiJaKkw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"Y8dNW6a_V18","kty":"RSA","alg":"RS384","n":"xQNISCAVvlsB4VTHq9HQcDf3PxF7D9DvnTNYPtXAxTIXx5bXVX4WxJU2xSTkYtN0k-yAMXQed9MAYNKsNwD7NAO7RV7m6jCSIgD1FEu3V6iEeliMetL4CfIe_Vn7Rb37lSI-gKaNMwBVIcYoAy7xOXLxxpSFJ5t357HbJnd3p0cgvx13sfyz-WyxqMLWY5IdxktwS-tdxUmpsk6M2xbcJB97c4h4afrfxp68ZB4fznC23aos6QUm7DLhGOURJAdwQTebUre9J6Vy3BXfKNpXb62AGpzPLGDzt-c-kQ05ckEzo9ZZZVC6l-DfMryb5rLZKlMKTefzL12ricSRcltcZw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"BSILu2VUSq8","kty":"RSA","alg":"RS512","n":"2OyR9CUp2B3_XrC1rwx3CxvsGenGyyjj5i_BMUyi8biEAu7N3aZ7AxvaSVtYGeWCWDRmPE2XImoEDtLBdG3wlOroOlRvgGnd3hlajqswIRgy3dmmbVETNqqJJQefc5tRESsA3VHKz04H3trcibo-ycM5HRc3cGXdWExg2XQUxkmOXKVCUEBnMpeWGlAG-QUGjGP3DVZ0V6-ldQXH_lP1ftt5zTWusOp0iyrLbvX7eWduVlfGsIHYNi3cVJdAxbZXUMwOwyHn3HUrlCDi1tc8_x8-pq2SgQhTrJQVF3D8UExYV_k6cTQOXRqJgz7LcISYyWULm8FM2NYWGl12MCMqqQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"xuMN0hE4aNA","kty":"RSA","alg":"RS256","n":"xs-BOX2tAPab_6ftuKFJNqJJPAMf6NnGEt_KPEuQKlS6Eoqxd1Sl3V6y8mj7g4TTg7Yb0JT0GjUmKs61cJww6w4JIQepzAKb_LT-mrOjckWTDC4lUSYm8IX-tfFDUKhkYh-rOQz7rNQ13BKQ_MHKGY3_imzp5tRvevkbwHzGjHRVMPKzRFBm20O5_IOSCFLYp0dIi-zKK7gSpZFfMW6ZoAoZiOhBoRhNFs-XJ6UUcAifNmpxnCDM9KJBGv7YCVroYnyt7pz0xSrab72ZGPQQo5EqnjvckO1ACQuekJfOCQ0c2yVd48y-W_wTDvSn1ZKOdecTE0BbQg2P-h1HYN3RFw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"hrVDwDlmtBc","kty":"RSA","alg":"RS384","n":"na2HnmI9weG040vd5v8mC9RkfzKmil-GtZxUNtCndW3MV_55x5yBund_TSo_rDHrlKm_ZvVWhvkhHtteZ-V_Yv521zA_vVaFVwCGQ0-KXSRW6GtereabW835tb23nQWItRepT1SX4Z_7tpS-_anpVVwaKvUqEJcUptFfkGICP98yMnemGkAR-ejLVNSElh4u9FU6q8Y4wBuBv_VRtcFanUcsnSDWIjCL0YyKZ1Ow7FqvGjpglBHsfzeWFyX2Hn2JZvozWNMGGm77ietL7fsPfvfAilrHXXFNk0Oso8DtQnj6Ft1oXLUyZijSiTN7AubpdaylW7tjbkXf42ZmPadjvw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"5DeLhvjbXpU","kty":"RSA","alg":"RS512","n":"xH5VCmySFeekK1oYflMd6XWV9PsNP8JBUbwhR0Uq4ANRPVdhzFc1N8GInEl-XWgBU9CYtLhMB4CrgRiFgSQPU7AUmYfmmaZ4ScGQItpIHcL5TSELw5ncQTmv4TYTEksvvESm-ihRbN6Irhrm-_izjzXZd1yRlpZJL-e4L5CGlIl4s1_ZwhHoF79Nw0_ql4Awn4hJQiZzdJnaJ36ltSVfIN750Glyv9MGVATpwKSsEtIiDHw8szcLXv04wPdmwTcblhgrSrgbPTn4YHpjmq6I6iFJz3sJEAGT-XbB7PdEC3Snk9CC8iJzaF-DrRVbp2BIi4Vo51AC1NPgESDU8lSWmQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"5lqnxcDvwtY","kty":"RSA","alg":"RS256","n":"nSn6UV7vgCImW0PExOhWUOqtT4_SM1ZShwN-Ti-4sIfiRgaOw1_Wf4PAHkqQmTp8xiOZhDOfe2NTDGhP0VENkwILPs_kdHq-Pm-4Qq4tx9nSEKdjq1XlEP99wmtmMQOSBdenwzkKzkXMMSROOqs3iItablA2vFnVfjZUsEioDikn6sQIg7nwQT6Sf76w1wv5uYrVlc-nU6FPh_08-h5C_IL2QNpbRBHM1BKtZEH2njDnSKVNFzwuwDfnjRtKwOtAmOwxxO0xXZHlDZYYE4tAlbAX1anJj_mjWxoLDPwQKvZCMw_XPLY3jo5nsSGOX2bBCWsZsZcbs_Cg0t58DldC2w","e":"AQAB","key_ops":["verify"],"ext":true}]}},"defaults":{"authenticate":{"response_type":"id_token token","display":"page","scope":["openid"]}},"registration":{"client_id":"eafafc5103e5b15ba06c3bed7c5dc3df","client_secret":"5eac22a963328151a139206a35036b17","redirect_uris":["https://localhost:7001/api/oidc/rp/https%3A%2F%2Flocalhost%3A7001"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:7001","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:7001/goodbye"],"frontchannel_logout_session_required":false,"registration_access_token":"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo3MDAxIiwic3ViIjoiZWFmYWZjNTEwM2U1YjE1YmEwNmMzYmVkN2M1ZGMzZGYiLCJhdWQiOiJlYWZhZmM1MTAzZTViMTViYTA2YzNiZWQ3YzVkYzNkZiJ9.atRg3STosJx0a9FV8cadbr0TpccgdTwqjsKQQYFv1hwDptgJr_WPYfp5mpUvNfr-Q7M4Ege5mONmw-JKa9rkqGzB5EOsEuJs1CfXZcFPjNZU10X_3iDnZik6eqYDVHmpwJrgVm3GIM3sN1VDqyksVUyr-Dd6vNbVnnHZOFHUpcZBSkvOb6bensDlgQ6TpZ-45TtPNgeHHjmhaL3t0xFeVABjUBLg_38yrxP_-Tylc7KMZVNCQCCDgGkgHBPyzf9KwHv8UU_MvHtQzYlV4_2u14iz9mLbMvbGMWv9akdGCfwZDldThbVSfmt_lz3dfRNivGGRnJNec9tdP2wT4mv4uQ","registration_client_uri":"https://localhost:7001/register/eafafc5103e5b15ba06c3bed7c5dc3df","client_id_issued_at":1489773546,"client_secret_expires_at":0}} \ No newline at end of file diff --git a/test/resources/accounts/db/oidc/op/clients/_key_7f81f70c7c306e96fdb5181f4c1d7222.json b/test/resources/accounts/db/oidc/op/clients/_key_7f81f70c7c306e96fdb5181f4c1d7222.json new file mode 100644 index 000000000..f83ca9ec7 --- /dev/null +++ b/test/resources/accounts/db/oidc/op/clients/_key_7f81f70c7c306e96fdb5181f4c1d7222.json @@ -0,0 +1 @@ +{"client_id":"7f81f70c7c306e96fdb5181f4c1d7222","client_secret":"f764cc06dbeeb2d908e10fb3b418853f","redirect_uris":["https://localhost:3457/api/oidc/rp/https%3A%2F%2Flocalhost%3A3457"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:3457","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:3457/goodbye"],"frontchannel_logout_session_required":false} \ No newline at end of file diff --git a/test/resources/accounts/db/oidc/op/provider.json b/test/resources/accounts/db/oidc/op/provider.json new file mode 100644 index 000000000..e14f25455 --- /dev/null +++ b/test/resources/accounts/db/oidc/op/provider.json @@ -0,0 +1,411 @@ +{ + "issuer": "https://localhost:3457", + "authorization_endpoint": "https://localhost:3457/authorize", + "token_endpoint": "https://localhost:3457/token", + "userinfo_endpoint": "https://localhost:3457/userinfo", + "jwks_uri": "https://localhost:3457/jwks", + "registration_endpoint": "https://localhost:3457/register", + "response_types_supported": [ + "code", + "code token", + "code id_token", + "id_token", + "id_token token", + "code id_token token", + "none" + ], + "response_modes_supported": [ + "query", + "fragment" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "client_credentials" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "none" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256" + ], + "display_values_supported": [], + "claim_types_supported": [ + "normal" + ], + "claims_supported": "", + "claims_parameter_supported": false, + "request_parameter_supported": false, + "request_uri_parameter_supported": true, + "require_request_uri_registration": false, + "check_session_iframe": "https://localhost:3457/session", + "end_session_endpoint": "https://localhost:3457/logout", + "keys": { + "descriptor": { + "id_token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + }, + "RS384": { + "alg": "RS384", + "modulusLength": 2048 + }, + "RS512": { + "alg": "RS512", + "modulusLength": 2048 + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "alg": "RS256", + "modulusLength": 2048 + } + } + } + }, + "jwks": { + "keys": [ + { + "kid": "lNZOB-DPE1k", + "kty": "RSA", + "alg": "RS256", + "n": "uvih8HfZj7Wu5Y8knLHxRY6v7oHL2jXWD-B6hXCreYhwaG9EEUt6Rp94p8-JBug3ywo8C_9dNg0RtQLEttcIC_vhqqlJI3pZxpGKXuD9h7XK-PppFVvgnfIGADG0Z-WzbcGDxlefStohR31Hjw5U3ioG3VtXGAYbqlOHM1l2UgDMJwBD5qwFmPP8gp5E2WQKCsuLvxDuOrkAbSDjw2zaI3RRmbLzdj4QkGej8GXhBptgM9RwcKmnoXu0sUdlootmcdiEg74yQ9M6EshNMhiv4k_W0rl7RqVOEL2PsAdmdbF_iWL8a90rGYOEILBrlU6bBR2mTvjV_Hvq-ifFy1YAmQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "Y38YKDtydoE", + "kty": "RSA", + "alg": "RS384", + "n": "tfgZKLjc8UMIblfAlVibJI_2uAxDNprn2VVLebS0sp6d1mtCXQkMYLlJ6e-7kavl8we391Ovnq5bRgpsFRq_LtRX9MpVlfioAUHwWPEG-R6vrQjgo4uynVhI3UEPHyNmZA5J4u34HNVTfAgmquomwwOmOv29ZNRxuYP1kVtscz1JeFPwg6LA7BxWrLc9ev4FQR6tjJKdo2kdLjAXR92odbCzJZ_jdYT3vIVCexMHxhoKnqCImkhfgKbGXcPHXWcelmuA2tzBaLut-Jjo0nJVQjRNDqy0Gyac0TptwFIxaiyHeTqugolUmEaJSfBSLszIRdlOTIGPJ7zdg5dJFK_Lxw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "WyMVv6BJ5Dk", + "kty": "RSA", + "alg": "RS512", + "n": "5JDlpbm2TjSW1wpdUZc5NHOqVVrNH_GumoODK_mk-MqImaIRpdR9b1ZJrK6FrW7HIF2bXvebD7olmp9a1goqe-ILbL_ORmhzlhRtyhjWQ-UOZqK5yOXqXXGQXgmok6TN-s55A-h_g12A7Yk5Y5S8EVa9EA4Axwqvm-Q_AkH0yS1qJo6BXYXb1fx205ucx-Ccot2LEBfxv8M7NOFTa-_G-sNchiKQMRoLhbZtLbSK2R1jkqGciEiRSLeXNG4nDu7Wd91-vhBixA1McxnzW96mW8lQwNXXo4gNH7SjONtYLlPQhZVEbmsQmXrOQN8a5RDkybFOIsbucItizSE9V_D7WQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "UykSj_HLgFA", + "kty": "RSA", + "alg": "RS256", + "n": "u79eQlGJN2XFNR-uEmPVtrB_ENRqaS81o6m63tZ5-PwhGHCwJ7rfVnnnvf6Ij_p91Z9pNpWBIVyZcw6UmQIoIBH-3BfxdaqhBxX9bf_N78TKj8_HU5IYjGijale4gog3kj9W2tJJO7R9iA43msjwLRD7pbAHp1iKFJgVTSXJlyLRbC82Dj4ivsEgJjPGvZt16OsGP5myIQwXEGzSPcEI0R9daZE5iM6xFZosaJ8B77eU-Aj3ciwxUBPi5BSZi2P1ZsF4QgSj3N7ZLbVKNW4FFr84IamA2YI0D7PyyNAE2PUZT8n0jHWRJKunuZuy5mgBY8H41KdBI6gNJqY90nHeJw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "BJDNTt8RpPE", + "kty": "RSA", + "alg": "RS384", + "n": "nXTd5AoT220nBkW6Zeax8caUI7_Tt0y4v9TEW8TOrzCVvhLBiKpQPjILUTfkGHzxPtysEzDQFSYdHWvg_fvGYItjJBunBMsKCNcb2_CDr2HXD6C0s62bAgct8bBSoaT1MLQ_3MaFKXSF3ZuB87X2B8CVUJ386HP2GY1kl54BuMdFELNZYhy9S_D0KHnQls52Vvb99X9WaYOyxvfr03PG-9EycnkWas5tn1pPFzT0DtJtBJ4IBtXQxTr98jpn_MCz1gRnMgzzkfSOcrMkkMXxePqxNINVKFXtRy7DaJiFOcCMbuK2RJUkSfY2uKcx0aKbp5Xhvix1W8N7c0Y90i6_6w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "z8iijSOOIs4", + "kty": "RSA", + "alg": "RS512", + "n": "rPCHP9XeTGOLf1Ezxeq_bdGdvYQZa993YcSVudT0EN6drTWqjykhUVEkT4MGAvLvax38kLARbPUTgMUV9UckDDWn6lRq4q6IZ5pytNOieQKZHzjEmQGzlbnEn1F2m1i5SAfBL-qsnt5q2RXMAiIUXk9q1ChJEHJxOZxnRIoQMc7yTsjjSdtIZKePFiYFn0nsl3A234ByyIBRjzZeoYEtTQKjDR7fP9LO78oZAgpwoGqmfI4IltqQYkFoqrN8I8l1yiJGyuvZRgDXUZ2fxGOQx2WD4xvlFL2TOCfN1UaPE9R4JdbRLLAOf5u1Sqnh4XTjDBhBbVodsmmbtvk4wFo-GQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + }, + { + "kid": "zD76wa11A2Y", + "kty": "RSA", + "alg": "RS256", + "n": "nMaSioq1An1J3tbkmc-zRrR8lkbP-WUVRuYhDxQvV-OcBw1R6cdyCcoeFJ1zuUT7ne6BlU6GMPRHuRKaH0KuOaiktUYtXm06T_HvtKFgCQSAKjMUj_ZHfTAJP8ahUsIc0D995XKp7nIGRF7Iy7I24QQFPRh7PmGlREZ52GJgYQgbm020-sWani0MqHoUFBlWxZW9NEqY1c3brN_qWnzjRKly6Kkk3sW1XHPcRLvoHnHQ6TKXJ8pfl-bNjTfK6zq9fDCZ_TY3qQZy66yT_2XPO6X0GHTdJsZlCj7Jg0qrilTHUkJra1bppTSAtVSQnSmYt_IV8zOYiVdJ3kw2khPcKw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + ] + }, + "id_token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "zg3jwCe0wbw", + "kty": "RSA", + "alg": "RS256", + "n": "uvih8HfZj7Wu5Y8knLHxRY6v7oHL2jXWD-B6hXCreYhwaG9EEUt6Rp94p8-JBug3ywo8C_9dNg0RtQLEttcIC_vhqqlJI3pZxpGKXuD9h7XK-PppFVvgnfIGADG0Z-WzbcGDxlefStohR31Hjw5U3ioG3VtXGAYbqlOHM1l2UgDMJwBD5qwFmPP8gp5E2WQKCsuLvxDuOrkAbSDjw2zaI3RRmbLzdj4QkGej8GXhBptgM9RwcKmnoXu0sUdlootmcdiEg74yQ9M6EshNMhiv4k_W0rl7RqVOEL2PsAdmdbF_iWL8a90rGYOEILBrlU6bBR2mTvjV_Hvq-ifFy1YAmQ", + "e": "AQAB", + "d": "TOxMG9YDQXfbLAD3bCxdemOZCESIbQ9nMYMGhW30bnzu2likpYTrGrEzf78Hvjq98aAVUk5OuBUqatFnw122ps-LaZ5aQ-lrlCF-z0g7pqDpkAPeRfZV9EWFqIDKm1BKwOYz499a3v3dYT8uuLGJwxmBV4Lj0zN4IFxbLIoq_tM22_I98jggeJIHc0ttUdwrEYlGlf9iv1_HWGceBpdIkNlfWFs5fPdlXqVja8Yc4GgRYE6MJmt_j7_bwSlQmZss3XzAhMVJq-KqLnm0P4lzUwo4TwNmeQkRjDNAdIClQUePIBvKaMsAKRCRjXtWnM4qDxmO-8tndWmY6Yx11fuz3Q", + "p": "53rZwi_S48PSfDc8tAqx_g6lS3SEk29UtwCXpZxTyxr1d9SMXsTrnh4G3Fk7nDNSLBoroIeHSyMb4AAMChU7UjSMhgHzmpVwXoU-xh0UdG9mD9FPqTY7dL88R3Vckq8qje0HOY2XNzosZo2QlLUXst0ay7p4ec6qFeqcqAFGm38", + "q": "zsbRydIPBp1dbiDgHKYoq_Ny_nQsIWIygBduvVWPnW4pwlmLUaGZq-qjOLJj51tLio2zq2UqA5goSvIu2wt34CCbrm9a22w5sFoC_krrbKd4M3CdmVIl8FIr6TN9Dn16b4oVFEwGk8wKYnnLSj33GH2A0QQJ_WWvScjc5fUcz-c", + "dp": "uYY-7WJDFgWmt6PV5T8FNWgrlvRGJZx_O0UgRb2rcweiYW5bKsGNTmcmfIiQPDrtyycWfEzjZJc5Cik_fP1TVCmFzwnVYroPG9KTY1l_QWrfVCIgRLCQqptzBprLnU0DQEkPF1OiNMNNPsyLaoRSACsyBMLpOEcpDvPApu6O1qU", + "dq": "jR2W0rtu0b7Xom8BQ8wJ-b-9fPZfn7DachyL0N7xkik6io59zAoTTAZnuivUjnH5zecC9TenQqi25t79JzRebTETzinkwdbMUBQ98rnCjXaFS-XRSG-NwMLzgMVI1XjA9BoyZJW172vSsn4YROShG6-bGAo_nxWkWSCh0LZFIYU", + "qi": "be9qPoheKmTGEvheYgCVdzoIYfk6o4svKUulUQWY9CUkUUo-VzVOJoFz7CQ98Aa13D-b3YDAczQsQ0l9Q98IegOuEe4N-dhjvGjnL3C7wdBrdKb6CXp7TllLLmOVQdJp9tcQKqriClCOdQhaoQrT_Nv8mZoqoYRDN_j-wCRMCXk", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "lNZOB-DPE1k", + "kty": "RSA", + "alg": "RS256", + "n": "uvih8HfZj7Wu5Y8knLHxRY6v7oHL2jXWD-B6hXCreYhwaG9EEUt6Rp94p8-JBug3ywo8C_9dNg0RtQLEttcIC_vhqqlJI3pZxpGKXuD9h7XK-PppFVvgnfIGADG0Z-WzbcGDxlefStohR31Hjw5U3ioG3VtXGAYbqlOHM1l2UgDMJwBD5qwFmPP8gp5E2WQKCsuLvxDuOrkAbSDjw2zaI3RRmbLzdj4QkGej8GXhBptgM9RwcKmnoXu0sUdlootmcdiEg74yQ9M6EshNMhiv4k_W0rl7RqVOEL2PsAdmdbF_iWL8a90rGYOEILBrlU6bBR2mTvjV_Hvq-ifFy1YAmQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "S0Mmez6wbi0", + "kty": "RSA", + "alg": "RS384", + "n": "tfgZKLjc8UMIblfAlVibJI_2uAxDNprn2VVLebS0sp6d1mtCXQkMYLlJ6e-7kavl8we391Ovnq5bRgpsFRq_LtRX9MpVlfioAUHwWPEG-R6vrQjgo4uynVhI3UEPHyNmZA5J4u34HNVTfAgmquomwwOmOv29ZNRxuYP1kVtscz1JeFPwg6LA7BxWrLc9ev4FQR6tjJKdo2kdLjAXR92odbCzJZ_jdYT3vIVCexMHxhoKnqCImkhfgKbGXcPHXWcelmuA2tzBaLut-Jjo0nJVQjRNDqy0Gyac0TptwFIxaiyHeTqugolUmEaJSfBSLszIRdlOTIGPJ7zdg5dJFK_Lxw", + "e": "AQAB", + "d": "T5f3mUJTEgyaEXm8uRaKtdFqv1JNzAKxyvRuEQwjxcVvkKxV2M_uZBhn5jWAO9WOvWDw7PPj62qkbdx9LjYGzfr_hglCqlibVAF9mcnDnQ_5E5zAdYjTVdOZ-31Lmfkn_jfpxaMFVcdRvvzpvPSyg7aC7WazgvkRzW2U6kGtDDJSfKR3QYxYxCWjzp1fe6GVBhcvBHx-UU90vxuRfP1mwqHXbaVpi2rAEwXb3rS1RhUjbhPzGHz8Hyozm4OUMh7fNJ2eqBdLtB9qo1II8YCXwLERy5C78ICwGVwZ2myAxBnpcUoDG7dr9zYwVJ_ICpKI7gJGol8lgMWOF70MRS6WAQ", + "p": "8axiZKAhOXdKtq8o3vNDjiPf9RSsB57r7ZsLlmT1aMqSfZnaXcPigUTvBFkJA_EFCQUlgspF5-YP5MYPdX9vR43_AEvNYldA52MfzsA8307sqHI54C4SeVoo9ooJ7Z-kWAIIFTsZwMUgqDRIf-Am4iy0rYK5Z5G8zyOY_c1vdoE", + "q": "wMGlrKCn_6fYAG_a8B-Eu-GtV0pOZ3S6kru80NOg00U78m3rJPzYGoDkm7gMKJCr0--uxecDg_rpBDo1vgkFa5Yf69V30tkC55D75QvawIINaYWL0dNILgU9MjX1JcL4hD2QwfyFyAbIStTkGJYs_C_wztPpMskSqNMGa7Ga7kc", + "dp": "OZEhguyt3V1wG6IPr0PtFJ-xClUZQVt2wYuMMA_ucT7HtEmAvZMakkZUVQnMXvb7hxGFxOjfzAR-RrVzGz72x-moE277BnDYUgXHnt0l4t-O-fTzmlX_Ko7ycP-iq8q6QAiD2mLQmJ2cUNTbbDJ9sKSLiUU5WtVZT1IgcFyOL4E", + "dq": "hRmykx9kok5-At86KTE6cJoHHg17UkjyRDxKx1A672gRWve3tZS6jKKQOU6_Zotveys4XgOFE--AU6D2V0DXc1D4vdproTakoM4mgiTLar7jEAhdYggpAU4w0akcnHSjMn1oper_Xf4A9FtJHgklCwb3m3oMvzrFHbqJ5nd_aiU", + "qi": "b-dDmbHrZIGvAPys-x703kFdVsaeq7mD4Gpux6Po0eR6tIHZq8doJN5Fg38dZ7Om2gqEysuUCKEeg7PGNRiQr8jbw8INFMQbx3plJCAUKmzeQyuozRbRiOh6iT0zhva7qf02brRDAqzg_XfjFBvLDNVg6nj5CTv3IitdL9o-mH8", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "Y38YKDtydoE", + "kty": "RSA", + "alg": "RS384", + "n": "tfgZKLjc8UMIblfAlVibJI_2uAxDNprn2VVLebS0sp6d1mtCXQkMYLlJ6e-7kavl8we391Ovnq5bRgpsFRq_LtRX9MpVlfioAUHwWPEG-R6vrQjgo4uynVhI3UEPHyNmZA5J4u34HNVTfAgmquomwwOmOv29ZNRxuYP1kVtscz1JeFPwg6LA7BxWrLc9ev4FQR6tjJKdo2kdLjAXR92odbCzJZ_jdYT3vIVCexMHxhoKnqCImkhfgKbGXcPHXWcelmuA2tzBaLut-Jjo0nJVQjRNDqy0Gyac0TptwFIxaiyHeTqugolUmEaJSfBSLszIRdlOTIGPJ7zdg5dJFK_Lxw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "Gypab0Oeig4", + "kty": "RSA", + "alg": "RS512", + "n": "5JDlpbm2TjSW1wpdUZc5NHOqVVrNH_GumoODK_mk-MqImaIRpdR9b1ZJrK6FrW7HIF2bXvebD7olmp9a1goqe-ILbL_ORmhzlhRtyhjWQ-UOZqK5yOXqXXGQXgmok6TN-s55A-h_g12A7Yk5Y5S8EVa9EA4Axwqvm-Q_AkH0yS1qJo6BXYXb1fx205ucx-Ccot2LEBfxv8M7NOFTa-_G-sNchiKQMRoLhbZtLbSK2R1jkqGciEiRSLeXNG4nDu7Wd91-vhBixA1McxnzW96mW8lQwNXXo4gNH7SjONtYLlPQhZVEbmsQmXrOQN8a5RDkybFOIsbucItizSE9V_D7WQ", + "e": "AQAB", + "d": "C3eZje77ToEk8DT86ZMEs7T53r5nfCrL78SZWCN2O2Ut5UdUS1WV1XgmyhsXadQc-Wq42NLjXK6iJFfKXuKzrvNwT0xwNOgYpHbx6ynD1jQvvMZ9O68NSxLfg1hItN-X1fV17NRFUXnndgNkdbKInPYVFjEdqN7IYLHa12ontdARMMomPOqGoHj8dGjypYZKYLtZmTiTaiq96pABS0sz2m6fWLtEqyt2l2_V_ksHkNIraUy5KujJfvzFHbBlE6g-pRJ6b032kUYAGLOVRYCvidmghjR7riDZZriNl5Z6XGaldzYCag8jiNZYehQE_MtgV8rWxHJqU14QHIH68CjUAQ", + "p": "-6U6A9mfrpUqh-x_z9Bpahyxjwu2cGmh0wI4ArrBa9mt3_oHKZeOv1e9n6M3Zj8Y9A0T-Ez5rLPWE0uODhRQchkbBvkBPMECrgNniKTT6wY2gYBrdOD9apekCimqoi-h_j1B9gX-mMHr4a54jx51_SewVBSIwHMiXEJDJeRQszE", + "q": "6IVuGo0uxt5vwkJAACk46vhViQ2N_bJ1fPZnnQxknLcAyxRP7KN_8uIao1TXoEDPyPHsIDYsSr_-eIGsLxY7zJk42gtpoWZYXwEpiwUPUn6Xm5D_umGk1QR8pEKSWZX5TmaTVeCEUxgwxKzVfWLq2u2_EIjbPRyGB5aKDBLJsKk", + "dp": "2_W3uUfPSSX_sCHsMnVEv0jnd1bQmH_swGmPFeuySBhU4JNHEXb1gpEqIdDkCs6afDC2RPLbxrbHJ8SCHhJpouII-tZK25UGR56YMBuLVULv_9CFnPtQ54w3Cd8T1IJ4Qae_8VGaEmJnUbRUkx0YGzlG6qesRTQeU7Bjy0o_s_E", + "dq": "mhOIkRmKrIbK4ZuK01B9gd4Kt-V-eGTfy21v3TZQGTR-1xLfnzv8VdKTujVHKM6poUsFn5amJOYyVmH-2bjO6VWCwaGcXjH2TwXzJEa3D4AJMDGV80gutGTjvujKF4j0iYoZCWfb5z_5WOn6EbsRSv8Ng4RcWpNjEPYlBbkRYvk", + "qi": "UliWSM1yB3u-RSXuSvpUXwjSBEhVkNnnxo0gnwRbtwq1cJYZH2Ps78COqDB2lSeuWA30KMyON53tXoMxIoRSUW1DXISctOw8Rou_FBYAGnczYe-_R4826121UmXa5mOJvpzloBJvqeNsomYoq3zP3MYiP3owu77FmckKrWFvhSU", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "WyMVv6BJ5Dk", + "kty": "RSA", + "alg": "RS512", + "n": "5JDlpbm2TjSW1wpdUZc5NHOqVVrNH_GumoODK_mk-MqImaIRpdR9b1ZJrK6FrW7HIF2bXvebD7olmp9a1goqe-ILbL_ORmhzlhRtyhjWQ-UOZqK5yOXqXXGQXgmok6TN-s55A-h_g12A7Yk5Y5S8EVa9EA4Axwqvm-Q_AkH0yS1qJo6BXYXb1fx205ucx-Ccot2LEBfxv8M7NOFTa-_G-sNchiKQMRoLhbZtLbSK2R1jkqGciEiRSLeXNG4nDu7Wd91-vhBixA1McxnzW96mW8lQwNXXo4gNH7SjONtYLlPQhZVEbmsQmXrOQN8a5RDkybFOIsbucItizSE9V_D7WQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "token": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "EOgk2OIxtdY", + "kty": "RSA", + "alg": "RS256", + "n": "u79eQlGJN2XFNR-uEmPVtrB_ENRqaS81o6m63tZ5-PwhGHCwJ7rfVnnnvf6Ij_p91Z9pNpWBIVyZcw6UmQIoIBH-3BfxdaqhBxX9bf_N78TKj8_HU5IYjGijale4gog3kj9W2tJJO7R9iA43msjwLRD7pbAHp1iKFJgVTSXJlyLRbC82Dj4ivsEgJjPGvZt16OsGP5myIQwXEGzSPcEI0R9daZE5iM6xFZosaJ8B77eU-Aj3ciwxUBPi5BSZi2P1ZsF4QgSj3N7ZLbVKNW4FFr84IamA2YI0D7PyyNAE2PUZT8n0jHWRJKunuZuy5mgBY8H41KdBI6gNJqY90nHeJw", + "e": "AQAB", + "d": "hxrSQNUd8kCJJo5ynIc9Tqc5-Slyndi8N9c3Q46B3ZvKUSUejqiaeUdmbcHSEhIHJYf-laoGb7SMNFDkvCriJxnsFgQg3TT5hfjcE6FGV-l8fvrdjJUQl1HhbvBLNZvCqbpszTEQRCfBQfxBJWC9_SBAht3i1BkR3HoIsiikJd5KyAcMnDxg8vgMtr7ORC7GEYHgtSbQggdfLvtTIGX3BqLaHwxefCL_BDGQQFdAaqwGQKkUYWkpTQBCP-v3rP0xVQj32y7jISU02imrGnNiT97EoB32IFqgEUXVBXaIBCoAzEZKi5WKjtQf7_K7NExv7ORUM-OWbYO2bcia6muhCQ", + "p": "7kfG0Xt7DAktGhNCjiNEZhiO4UWJKqvt1c8Q7Ak6XVFErlsoLR06Sm2NJJNCP3M4j2aE80wmVRufq_G5CExEGivNLtfBoIGD7mNpaoIuDswUH_8Jkj-om81JR3_9OCr97MPbrZNHpeLdWdE49QcaVqQL7zjXU9FGlfrMVl6nqlU", + "q": "ybWUsy1-TScC-QPLc5djDSRGZdln1TvZ5tSICRPZA3zN4qmxTlZ0tdNves20sVKdeBFEU6XBteRFUaWpSQkIcrsCdd8P4hDA3UySGOFe6NxjaBaJ0PCDRICtyCv8WvcuYRAlMgmT2WVu1u_r_6l9ceW7_eS0ErohIXDZWiDs2os", + "dp": "iy3Rq7p8fOM_POPTFEL1SM0_Z8W-APa7zQ9NyxD4zlkRzOXh6bgQvDiRILQDFhyvBNPVBGeOXFfuQ_jFI1uoy8CZ8KqFpsL_1NasVFIFpQ7_ElFdvdcBHUAjdWgE-DHkb89XGWPVjcedk0DqC_VCJSlc7zY8T_EFUcVUZX6UYKE", + "dq": "X3B0QGdZKGY6CNrbzACoVFKCoLRCZelgy9Bp4Wmrt_O4cvP5ueg8Zr_5MnDcez5s1Z_N5Yo7YrX0epJYy_7jKW4E1wLJQBzPNKaDRhR01NdajaiEYwE6CxKbp2fwipYEMtbx0oAnnahZzodM8fYfLeIWliY9cdLx1CHSJcwIZcs", + "qi": "jj5sWN5qgk7cutLBPZwLztCOCAi_zf1xVnTRCLc69-jllf5J0lzd867xQB-7C02GRJsKEP79I3cevRBIYFT5u7M_5Ype_-ZpF4B0_YTC226eXk7mT2kPIu25ezjAlOQirKLS32LQeC1niyLVzJwFGzvDbuztExwQy4AA0wzBa-s", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "UykSj_HLgFA", + "kty": "RSA", + "alg": "RS256", + "n": "u79eQlGJN2XFNR-uEmPVtrB_ENRqaS81o6m63tZ5-PwhGHCwJ7rfVnnnvf6Ij_p91Z9pNpWBIVyZcw6UmQIoIBH-3BfxdaqhBxX9bf_N78TKj8_HU5IYjGijale4gog3kj9W2tJJO7R9iA43msjwLRD7pbAHp1iKFJgVTSXJlyLRbC82Dj4ivsEgJjPGvZt16OsGP5myIQwXEGzSPcEI0R9daZE5iM6xFZosaJ8B77eU-Aj3ciwxUBPi5BSZi2P1ZsF4QgSj3N7ZLbVKNW4FFr84IamA2YI0D7PyyNAE2PUZT8n0jHWRJKunuZuy5mgBY8H41KdBI6gNJqY90nHeJw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS384": { + "privateJwk": { + "kid": "74RgcVvN2XQ", + "kty": "RSA", + "alg": "RS384", + "n": "nXTd5AoT220nBkW6Zeax8caUI7_Tt0y4v9TEW8TOrzCVvhLBiKpQPjILUTfkGHzxPtysEzDQFSYdHWvg_fvGYItjJBunBMsKCNcb2_CDr2HXD6C0s62bAgct8bBSoaT1MLQ_3MaFKXSF3ZuB87X2B8CVUJ386HP2GY1kl54BuMdFELNZYhy9S_D0KHnQls52Vvb99X9WaYOyxvfr03PG-9EycnkWas5tn1pPFzT0DtJtBJ4IBtXQxTr98jpn_MCz1gRnMgzzkfSOcrMkkMXxePqxNINVKFXtRy7DaJiFOcCMbuK2RJUkSfY2uKcx0aKbp5Xhvix1W8N7c0Y90i6_6w", + "e": "AQAB", + "d": "JDkvlveui8t3r9y4vhqtk0aw6-yEo2W2FwO2zLnhJrKRsHGWLn-oCDbxHZUzF2MfCOwxdhTDuinThuI7l5Kll65Zd4QZD2Q6gcAi-51AXkeu9zxsde2ZfIT0T-bVi7RsZ_D_xCWzms7gwRO2eL_CwxPOBgbimQOceVJq8up50O1ChI7rk4XHP39SrphrRlvh7A7O83U3rVF68cYV7ik_mCTdLHB99h88qP1EMV_QLOIofgymqDQDScodS9MQ7QCdoAoLcsXnu5Hqo3amVIo1DBDzn61QSKv6cDTuqBmufC03RtlCdRAwsDkxAh09DvxLzaOBbKC02ii0tbXPrvO_WQ", + "p": "0TucWJBNp8Ts03KFJ1feONy01loYa0iG0Dka4BbtU1-1nBxmfhoIBYpLc_D5tRoR90bC1pS7mJcvtaMesuhybgkP5dIR-KFPdD9aTXCtmdLKHGrvgL6_xrbQBz2Ao20TYAvf_hHCECclBjvpPCc5KWsLZpYgjCMesxC8un9G2-U", + "q": "wKaXsEc7sFvci6YpvcLIGIf7gLpaUUBhWSbU0Njpcj6paWLl2ODPDYbWxQf5Th-4pJn-OhLjD8n6HV5vQDD1PQuHufQnj1DbVNMIzW1GWMyUmIMhbCiVo67KgZr_3PIfEG_5TsRuBVb_SOpkOFLKqFE71LiXQPCOY5ZmlFdhj48", + "dp": "HFgE8AJsYqPMqUBERXYjxnQvkzIVSMNEcASsXVr9v2OhyIoYYFDKcWWwnv4v9ZaYhHTzg_oWB6_DaMm2KOpQRhO4MZvpj1La3paOdxsiiUoC0yKxWzF77UFqoPB18q2eCE7TgymIroN_An8vM1Tk63Vyz-zab-F6ESvdRS5kvPk", + "dq": "FUhSKZ808N61FphctCH4iP08w5PStncuSfMIP6o23_AcNxA95B-xwATNZSbkW8UVWNnKRBAiFXRytRvhnm3KKdxEOj7GwAZmtJA7wLX5t4WiRNb3skMphNOie37sFTSKSf6UxCbfIKfju-Jo_-_lg4K14WIjE4F_uXC8FFcy5_E", + "qi": "FQ2MgA9yEWL3PF7Z8-BVKvc4VjpFDfhYcPwCUVXI2jw7tdzQpsc86uc5ktBoBjGBk_k1rMDPaTcBQNc-a-MOjzjBeZAW8XxCZq0N3-Is7JOje1ajMTRZSL9xcrj5yrnuj4weM_jEArnYU8bwhzmzsd_p-MfMQ0QuvOlVCiqS4_4", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "BJDNTt8RpPE", + "kty": "RSA", + "alg": "RS384", + "n": "nXTd5AoT220nBkW6Zeax8caUI7_Tt0y4v9TEW8TOrzCVvhLBiKpQPjILUTfkGHzxPtysEzDQFSYdHWvg_fvGYItjJBunBMsKCNcb2_CDr2HXD6C0s62bAgct8bBSoaT1MLQ_3MaFKXSF3ZuB87X2B8CVUJ386HP2GY1kl54BuMdFELNZYhy9S_D0KHnQls52Vvb99X9WaYOyxvfr03PG-9EycnkWas5tn1pPFzT0DtJtBJ4IBtXQxTr98jpn_MCz1gRnMgzzkfSOcrMkkMXxePqxNINVKFXtRy7DaJiFOcCMbuK2RJUkSfY2uKcx0aKbp5Xhvix1W8N7c0Y90i6_6w", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + }, + "RS512": { + "privateJwk": { + "kid": "WHZGgfOLfQk", + "kty": "RSA", + "alg": "RS512", + "n": "rPCHP9XeTGOLf1Ezxeq_bdGdvYQZa993YcSVudT0EN6drTWqjykhUVEkT4MGAvLvax38kLARbPUTgMUV9UckDDWn6lRq4q6IZ5pytNOieQKZHzjEmQGzlbnEn1F2m1i5SAfBL-qsnt5q2RXMAiIUXk9q1ChJEHJxOZxnRIoQMc7yTsjjSdtIZKePFiYFn0nsl3A234ByyIBRjzZeoYEtTQKjDR7fP9LO78oZAgpwoGqmfI4IltqQYkFoqrN8I8l1yiJGyuvZRgDXUZ2fxGOQx2WD4xvlFL2TOCfN1UaPE9R4JdbRLLAOf5u1Sqnh4XTjDBhBbVodsmmbtvk4wFo-GQ", + "e": "AQAB", + "d": "qfaGZeVx4U9f9NPAdz37vxlo1q3yMgNgl9SVdhpleALhoi6BHsvEc9-0OPTDPri1Nmg1JZn0tkmyTjbkGrg9JEbDbVhj576yTmgLXc40-orkJDwtc1apwXfeVtnAIHK1PaZpZgdUeZqMFigG5P3LWNjiW_nvvNtMjds53rF1swxboAXxfNA_jY6JNq2mAbgMKk1yON7yQFDFs0_Z7cghj9J06_83uZ8_QDPETtmLpF9lJ0WJDR4DjqSz21tLbUjQ8Ry3SVUw19E7ZEhh0CGnGAnHcV-OGCJcbJ6WgwxkjxgxGzy2cfAuHFob581i6XIn7yBuSuwOrgObP1HEwuoelQ", + "p": "0sgIC6xdVNyghzkLr3qiK1yOXtyZnBuwrJhq5fB_2_OqRkrij1LqU7ONRYTzuuN8SxwTQqwBkrCLuiOYjEuge1AVZuJdS2hAF5GcllOdKf-SGnatxzqs3TMJaez490cUUgeN0grP-Bs9XCXK0hQUght9Pk7Yl7_le4bXciEci_c", + "q": "0gpAK6WsOuZpdGOn0lL5nbo85VQ2odTqK7n7Mo6d3jFvpomUwpTSupm-dKb4OxD4SZFpgI_eZ9trCWopXZStqFmSO5koiZPdZKRmvxsDYgQpJ2iog3e5UsAensnhw32a-UJfCn9Bjm9ORyOyml2L_5bAEttv6dIfOr8gROiDYm8", + "dp": "kuQj9z6frEw08wemRRxJd76A2UsTId-KOD3gAW6hLD-bInF9gjReaQZwJUqKMGvoas-d_JCyZ-_w8D9uSBdMN6OPxqtqKOr1_3bSkVCj7mjVAOxEHtudLGos3UzwFCPM3X22L_KpDFavZFBSECU-RY2ysoFwIBDzdCp8amT45_E", + "dq": "eb3bR_E1DMa0ZPPGOBBEAnoKBdpz-AUS3dlkkf873afF0T95a_ca1XF7hN2qj4Hch7ey8QNyo7v4JHLWGxmsNiIEsmqppmSANG9d5nLf2RYUTHVLBziDwET--oaFRuwswUEJGWp9MvOs6Wr1gKesF67nEYcDLQHPfBt_trEWRh0", + "qi": "kTuR7DcWJfwEb1BxDGsIRqHfiJsyXcQJFzdSiHF1XwO_akMaCffZMUT-5cb6GOZcOa95Tm7huWqJNrGz_dpDh92-z_Z7LxQbmBUZHVDtDaWIOhdU9TmJahJjzsihE8qmv2sYO_klofHEEqLDA4HgBSvlUbu5awZqLQV993_tH_E", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "z8iijSOOIs4", + "kty": "RSA", + "alg": "RS512", + "n": "rPCHP9XeTGOLf1Ezxeq_bdGdvYQZa993YcSVudT0EN6drTWqjykhUVEkT4MGAvLvax38kLARbPUTgMUV9UckDDWn6lRq4q6IZ5pytNOieQKZHzjEmQGzlbnEn1F2m1i5SAfBL-qsnt5q2RXMAiIUXk9q1ChJEHJxOZxnRIoQMc7yTsjjSdtIZKePFiYFn0nsl3A234ByyIBRjzZeoYEtTQKjDR7fP9LO78oZAgpwoGqmfI4IltqQYkFoqrN8I8l1yiJGyuvZRgDXUZ2fxGOQx2WD4xvlFL2TOCfN1UaPE9R4JdbRLLAOf5u1Sqnh4XTjDBhBbVodsmmbtvk4wFo-GQ", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + }, + "encryption": {} + }, + "userinfo": { + "encryption": {} + }, + "register": { + "signing": { + "RS256": { + "privateJwk": { + "kid": "yYejIS23YgQ", + "kty": "RSA", + "alg": "RS256", + "n": "nMaSioq1An1J3tbkmc-zRrR8lkbP-WUVRuYhDxQvV-OcBw1R6cdyCcoeFJ1zuUT7ne6BlU6GMPRHuRKaH0KuOaiktUYtXm06T_HvtKFgCQSAKjMUj_ZHfTAJP8ahUsIc0D995XKp7nIGRF7Iy7I24QQFPRh7PmGlREZ52GJgYQgbm020-sWani0MqHoUFBlWxZW9NEqY1c3brN_qWnzjRKly6Kkk3sW1XHPcRLvoHnHQ6TKXJ8pfl-bNjTfK6zq9fDCZ_TY3qQZy66yT_2XPO6X0GHTdJsZlCj7Jg0qrilTHUkJra1bppTSAtVSQnSmYt_IV8zOYiVdJ3kw2khPcKw", + "e": "AQAB", + "d": "jlnTp45Iy0jd8UPocCzimLm7Qmxr3QTGrAi7TcjDMCeQfeq_TOl3B6KJa6iH8lrLqVxuNxcEy6CTG13jqazPK7WQULS27z14rCx-veGlpKp8gVS-P_WcdfRPAaSmyNFOflyYQOW7nLHWNPBnnGVmZSxUYWRAS6U3_eWML0ksQA1DmIqScT98UcxRXI2wY7UDVGJw-U8yIfCTuYoF8kcx_IjlAcEEpUsARtR52FB4iw8WPtXueLafVOCUssbZ44Kcn0nHNOeaS14qAnCbUTlO3KdtRQdY3rVjxmdyIgyHZkZ9KSi4fLmH5fbQH8-M4kJrODjX8qpZpc-MVsXAn2LmKQ", + "p": "zxoUYABmktftPxkut8qRsEfDVu7UpvfFMYDKHCgbdzHJicYzdOlkl-dAn7vQwj4AGE_mUb2lLpgnpBlqnpWBbBcXE6Xy9ofqf4heFeS5xkOvYZzbHAk9_7KC_8MT7YD1fYYz3b4iMeY-TZDwg3cWu4vYCObG3B2BP0L2OhrK3sU", + "q": "wcqcflNQ0Gbmo4Ms5e7LWvWVFNKF50MkG9XSf5OAkmj_SG8x1HbNfS2pum8oHvuNhPdkDkBdG9o6ruY5XbHS1kElYzmSLsBSCurWxMCqt98ZwAa6utALNZ8QdlvDY_Y8wif0_gwvjGEnOaPKxrQ8nOokm32IM_pTm6XYZmtwfi8", + "dp": "bOy1jKyJRnBk6ovvI2FacNG9rqpclBi60UeAhYCeuXkpG9pv0-yxKKfLOHgK2y7K0_6qD5HkH_aM2uU3S4Msl9IpI_9jI0DnF_58JZ2wC9QrmPZr03oU7rhP5_8NKxxpgYSlINpQl9gWKquxpCNthGSP0la2fqzR_pjUckkHLFU", + "dq": "mw4XOshE8Ap1Xb02Ll9rXEME3p03QHurJ45lF2iYxgy2vWki0KGh9xeTJzWLP4b8i7g52WFMXl20-H4CxmHilUWYuZS1zyxYOJ3_63tQ3T_n5Yo82_5cCbJUxK7VXmUF5j98OczcOpD9hpP0ShqqKM77LWI6mYQgY3hF9mTepEc", + "qi": "L4PcNqm9poaaMOs70jPZLOJ98HOEMeO95ma_ShiowBJY6jHNkFnbJhdhj2guA7WJ7MziIVMcgOd66Jcear-4VUgezTRQsSIT5BTtkZGKCNkdgWHyK5roBDkc6hXRwwpIBosU-AYRZ4NtSL1nIa4hKLHD3jTVDsnB4X2wBhhggsc", + "key_ops": [ + "sign" + ], + "ext": true + }, + "publicJwk": { + "kid": "zD76wa11A2Y", + "kty": "RSA", + "alg": "RS256", + "n": "nMaSioq1An1J3tbkmc-zRrR8lkbP-WUVRuYhDxQvV-OcBw1R6cdyCcoeFJ1zuUT7ne6BlU6GMPRHuRKaH0KuOaiktUYtXm06T_HvtKFgCQSAKjMUj_ZHfTAJP8ahUsIc0D995XKp7nIGRF7Iy7I24QQFPRh7PmGlREZ52GJgYQgbm020-sWani0MqHoUFBlWxZW9NEqY1c3brN_qWnzjRKly6Kkk3sW1XHPcRLvoHnHQ6TKXJ8pfl-bNjTfK6zq9fDCZ_TY3qQZy66yT_2XPO6X0GHTdJsZlCj7Jg0qrilTHUkJra1bppTSAtVSQnSmYt_IV8zOYiVdJ3kw2khPcKw", + "e": "AQAB", + "key_ops": [ + "verify" + ], + "ext": true + } + } + } + }, + "jwkSet": "{\"keys\":[{\"kid\":\"lNZOB-DPE1k\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"uvih8HfZj7Wu5Y8knLHxRY6v7oHL2jXWD-B6hXCreYhwaG9EEUt6Rp94p8-JBug3ywo8C_9dNg0RtQLEttcIC_vhqqlJI3pZxpGKXuD9h7XK-PppFVvgnfIGADG0Z-WzbcGDxlefStohR31Hjw5U3ioG3VtXGAYbqlOHM1l2UgDMJwBD5qwFmPP8gp5E2WQKCsuLvxDuOrkAbSDjw2zaI3RRmbLzdj4QkGej8GXhBptgM9RwcKmnoXu0sUdlootmcdiEg74yQ9M6EshNMhiv4k_W0rl7RqVOEL2PsAdmdbF_iWL8a90rGYOEILBrlU6bBR2mTvjV_Hvq-ifFy1YAmQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"Y38YKDtydoE\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"tfgZKLjc8UMIblfAlVibJI_2uAxDNprn2VVLebS0sp6d1mtCXQkMYLlJ6e-7kavl8we391Ovnq5bRgpsFRq_LtRX9MpVlfioAUHwWPEG-R6vrQjgo4uynVhI3UEPHyNmZA5J4u34HNVTfAgmquomwwOmOv29ZNRxuYP1kVtscz1JeFPwg6LA7BxWrLc9ev4FQR6tjJKdo2kdLjAXR92odbCzJZ_jdYT3vIVCexMHxhoKnqCImkhfgKbGXcPHXWcelmuA2tzBaLut-Jjo0nJVQjRNDqy0Gyac0TptwFIxaiyHeTqugolUmEaJSfBSLszIRdlOTIGPJ7zdg5dJFK_Lxw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"WyMVv6BJ5Dk\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"5JDlpbm2TjSW1wpdUZc5NHOqVVrNH_GumoODK_mk-MqImaIRpdR9b1ZJrK6FrW7HIF2bXvebD7olmp9a1goqe-ILbL_ORmhzlhRtyhjWQ-UOZqK5yOXqXXGQXgmok6TN-s55A-h_g12A7Yk5Y5S8EVa9EA4Axwqvm-Q_AkH0yS1qJo6BXYXb1fx205ucx-Ccot2LEBfxv8M7NOFTa-_G-sNchiKQMRoLhbZtLbSK2R1jkqGciEiRSLeXNG4nDu7Wd91-vhBixA1McxnzW96mW8lQwNXXo4gNH7SjONtYLlPQhZVEbmsQmXrOQN8a5RDkybFOIsbucItizSE9V_D7WQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"UykSj_HLgFA\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"u79eQlGJN2XFNR-uEmPVtrB_ENRqaS81o6m63tZ5-PwhGHCwJ7rfVnnnvf6Ij_p91Z9pNpWBIVyZcw6UmQIoIBH-3BfxdaqhBxX9bf_N78TKj8_HU5IYjGijale4gog3kj9W2tJJO7R9iA43msjwLRD7pbAHp1iKFJgVTSXJlyLRbC82Dj4ivsEgJjPGvZt16OsGP5myIQwXEGzSPcEI0R9daZE5iM6xFZosaJ8B77eU-Aj3ciwxUBPi5BSZi2P1ZsF4QgSj3N7ZLbVKNW4FFr84IamA2YI0D7PyyNAE2PUZT8n0jHWRJKunuZuy5mgBY8H41KdBI6gNJqY90nHeJw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"BJDNTt8RpPE\",\"kty\":\"RSA\",\"alg\":\"RS384\",\"n\":\"nXTd5AoT220nBkW6Zeax8caUI7_Tt0y4v9TEW8TOrzCVvhLBiKpQPjILUTfkGHzxPtysEzDQFSYdHWvg_fvGYItjJBunBMsKCNcb2_CDr2HXD6C0s62bAgct8bBSoaT1MLQ_3MaFKXSF3ZuB87X2B8CVUJ386HP2GY1kl54BuMdFELNZYhy9S_D0KHnQls52Vvb99X9WaYOyxvfr03PG-9EycnkWas5tn1pPFzT0DtJtBJ4IBtXQxTr98jpn_MCz1gRnMgzzkfSOcrMkkMXxePqxNINVKFXtRy7DaJiFOcCMbuK2RJUkSfY2uKcx0aKbp5Xhvix1W8N7c0Y90i6_6w\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"z8iijSOOIs4\",\"kty\":\"RSA\",\"alg\":\"RS512\",\"n\":\"rPCHP9XeTGOLf1Ezxeq_bdGdvYQZa993YcSVudT0EN6drTWqjykhUVEkT4MGAvLvax38kLARbPUTgMUV9UckDDWn6lRq4q6IZ5pytNOieQKZHzjEmQGzlbnEn1F2m1i5SAfBL-qsnt5q2RXMAiIUXk9q1ChJEHJxOZxnRIoQMc7yTsjjSdtIZKePFiYFn0nsl3A234ByyIBRjzZeoYEtTQKjDR7fP9LO78oZAgpwoGqmfI4IltqQYkFoqrN8I8l1yiJGyuvZRgDXUZ2fxGOQx2WD4xvlFL2TOCfN1UaPE9R4JdbRLLAOf5u1Sqnh4XTjDBhBbVodsmmbtvk4wFo-GQ\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true},{\"kid\":\"zD76wa11A2Y\",\"kty\":\"RSA\",\"alg\":\"RS256\",\"n\":\"nMaSioq1An1J3tbkmc-zRrR8lkbP-WUVRuYhDxQvV-OcBw1R6cdyCcoeFJ1zuUT7ne6BlU6GMPRHuRKaH0KuOaiktUYtXm06T_HvtKFgCQSAKjMUj_ZHfTAJP8ahUsIc0D995XKp7nIGRF7Iy7I24QQFPRh7PmGlREZ52GJgYQgbm020-sWani0MqHoUFBlWxZW9NEqY1c3brN_qWnzjRKly6Kkk3sW1XHPcRLvoHnHQ6TKXJ8pfl-bNjTfK6zq9fDCZ_TY3qQZy66yT_2XPO6X0GHTdJsZlCj7Jg0qrilTHUkJra1bppTSAtVSQnSmYt_IV8zOYiVdJ3kw2khPcKw\",\"e\":\"AQAB\",\"key_ops\":[\"verify\"],\"ext\":true}]}" + } +} \ No newline at end of file diff --git a/test/resources/accounts/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A3457.json b/test/resources/accounts/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A3457.json new file mode 100644 index 000000000..d954f81aa --- /dev/null +++ b/test/resources/accounts/db/oidc/rp/clients/_key_https%3A%2F%2Flocalhost%3A3457.json @@ -0,0 +1 @@ +{"provider":{"url":"https://localhost:3457","configuration":{"issuer":"https://localhost:3457","authorization_endpoint":"https://localhost:3457/authorize","token_endpoint":"https://localhost:3457/token","userinfo_endpoint":"https://localhost:3457/userinfo","jwks_uri":"https://localhost:3457/jwks","registration_endpoint":"https://localhost:3457/register","response_types_supported":["code","code token","code id_token","id_token","id_token token","code id_token token","none"],"response_modes_supported":["query","fragment"],"grant_types_supported":["authorization_code","implicit","refresh_token","client_credentials"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256","RS384","RS512","none"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"token_endpoint_auth_signing_alg_values_supported":["RS256"],"display_values_supported":[],"claim_types_supported":["normal"],"claims_supported":"","claims_parameter_supported":false,"request_parameter_supported":false,"request_uri_parameter_supported":true,"require_request_uri_registration":false,"check_session_iframe":"https://localhost:3457/session","end_session_endpoint":"https://localhost:3457/logout"},"jwks":{"keys":[{"kid":"lNZOB-DPE1k","kty":"RSA","alg":"RS256","n":"uvih8HfZj7Wu5Y8knLHxRY6v7oHL2jXWD-B6hXCreYhwaG9EEUt6Rp94p8-JBug3ywo8C_9dNg0RtQLEttcIC_vhqqlJI3pZxpGKXuD9h7XK-PppFVvgnfIGADG0Z-WzbcGDxlefStohR31Hjw5U3ioG3VtXGAYbqlOHM1l2UgDMJwBD5qwFmPP8gp5E2WQKCsuLvxDuOrkAbSDjw2zaI3RRmbLzdj4QkGej8GXhBptgM9RwcKmnoXu0sUdlootmcdiEg74yQ9M6EshNMhiv4k_W0rl7RqVOEL2PsAdmdbF_iWL8a90rGYOEILBrlU6bBR2mTvjV_Hvq-ifFy1YAmQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"Y38YKDtydoE","kty":"RSA","alg":"RS384","n":"tfgZKLjc8UMIblfAlVibJI_2uAxDNprn2VVLebS0sp6d1mtCXQkMYLlJ6e-7kavl8we391Ovnq5bRgpsFRq_LtRX9MpVlfioAUHwWPEG-R6vrQjgo4uynVhI3UEPHyNmZA5J4u34HNVTfAgmquomwwOmOv29ZNRxuYP1kVtscz1JeFPwg6LA7BxWrLc9ev4FQR6tjJKdo2kdLjAXR92odbCzJZ_jdYT3vIVCexMHxhoKnqCImkhfgKbGXcPHXWcelmuA2tzBaLut-Jjo0nJVQjRNDqy0Gyac0TptwFIxaiyHeTqugolUmEaJSfBSLszIRdlOTIGPJ7zdg5dJFK_Lxw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"WyMVv6BJ5Dk","kty":"RSA","alg":"RS512","n":"5JDlpbm2TjSW1wpdUZc5NHOqVVrNH_GumoODK_mk-MqImaIRpdR9b1ZJrK6FrW7HIF2bXvebD7olmp9a1goqe-ILbL_ORmhzlhRtyhjWQ-UOZqK5yOXqXXGQXgmok6TN-s55A-h_g12A7Yk5Y5S8EVa9EA4Axwqvm-Q_AkH0yS1qJo6BXYXb1fx205ucx-Ccot2LEBfxv8M7NOFTa-_G-sNchiKQMRoLhbZtLbSK2R1jkqGciEiRSLeXNG4nDu7Wd91-vhBixA1McxnzW96mW8lQwNXXo4gNH7SjONtYLlPQhZVEbmsQmXrOQN8a5RDkybFOIsbucItizSE9V_D7WQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"UykSj_HLgFA","kty":"RSA","alg":"RS256","n":"u79eQlGJN2XFNR-uEmPVtrB_ENRqaS81o6m63tZ5-PwhGHCwJ7rfVnnnvf6Ij_p91Z9pNpWBIVyZcw6UmQIoIBH-3BfxdaqhBxX9bf_N78TKj8_HU5IYjGijale4gog3kj9W2tJJO7R9iA43msjwLRD7pbAHp1iKFJgVTSXJlyLRbC82Dj4ivsEgJjPGvZt16OsGP5myIQwXEGzSPcEI0R9daZE5iM6xFZosaJ8B77eU-Aj3ciwxUBPi5BSZi2P1ZsF4QgSj3N7ZLbVKNW4FFr84IamA2YI0D7PyyNAE2PUZT8n0jHWRJKunuZuy5mgBY8H41KdBI6gNJqY90nHeJw","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"BJDNTt8RpPE","kty":"RSA","alg":"RS384","n":"nXTd5AoT220nBkW6Zeax8caUI7_Tt0y4v9TEW8TOrzCVvhLBiKpQPjILUTfkGHzxPtysEzDQFSYdHWvg_fvGYItjJBunBMsKCNcb2_CDr2HXD6C0s62bAgct8bBSoaT1MLQ_3MaFKXSF3ZuB87X2B8CVUJ386HP2GY1kl54BuMdFELNZYhy9S_D0KHnQls52Vvb99X9WaYOyxvfr03PG-9EycnkWas5tn1pPFzT0DtJtBJ4IBtXQxTr98jpn_MCz1gRnMgzzkfSOcrMkkMXxePqxNINVKFXtRy7DaJiFOcCMbuK2RJUkSfY2uKcx0aKbp5Xhvix1W8N7c0Y90i6_6w","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"z8iijSOOIs4","kty":"RSA","alg":"RS512","n":"rPCHP9XeTGOLf1Ezxeq_bdGdvYQZa993YcSVudT0EN6drTWqjykhUVEkT4MGAvLvax38kLARbPUTgMUV9UckDDWn6lRq4q6IZ5pytNOieQKZHzjEmQGzlbnEn1F2m1i5SAfBL-qsnt5q2RXMAiIUXk9q1ChJEHJxOZxnRIoQMc7yTsjjSdtIZKePFiYFn0nsl3A234ByyIBRjzZeoYEtTQKjDR7fP9LO78oZAgpwoGqmfI4IltqQYkFoqrN8I8l1yiJGyuvZRgDXUZ2fxGOQx2WD4xvlFL2TOCfN1UaPE9R4JdbRLLAOf5u1Sqnh4XTjDBhBbVodsmmbtvk4wFo-GQ","e":"AQAB","key_ops":["verify"],"ext":true},{"kid":"zD76wa11A2Y","kty":"RSA","alg":"RS256","n":"nMaSioq1An1J3tbkmc-zRrR8lkbP-WUVRuYhDxQvV-OcBw1R6cdyCcoeFJ1zuUT7ne6BlU6GMPRHuRKaH0KuOaiktUYtXm06T_HvtKFgCQSAKjMUj_ZHfTAJP8ahUsIc0D995XKp7nIGRF7Iy7I24QQFPRh7PmGlREZ52GJgYQgbm020-sWani0MqHoUFBlWxZW9NEqY1c3brN_qWnzjRKly6Kkk3sW1XHPcRLvoHnHQ6TKXJ8pfl-bNjTfK6zq9fDCZ_TY3qQZy66yT_2XPO6X0GHTdJsZlCj7Jg0qrilTHUkJra1bppTSAtVSQnSmYt_IV8zOYiVdJ3kw2khPcKw","e":"AQAB","key_ops":["verify"],"ext":true}]}},"defaults":{"authenticate":{"response_type":"id_token token","display":"page","scope":["openid"]}},"registration":{"client_id":"7f81f70c7c306e96fdb5181f4c1d7222","client_secret":"f764cc06dbeeb2d908e10fb3b418853f","redirect_uris":["https://localhost:3457/api/oidc/rp/https%3A%2F%2Flocalhost%3A3457"],"response_types":["code","id_token token","code id_token token"],"grant_types":["authorization_code","implicit","refresh_token","client_credentials"],"application_type":"web","client_name":"Solid OIDC RP for https://localhost:3457","id_token_signed_response_alg":"RS256","token_endpoint_auth_method":"client_secret_basic","default_max_age":86400,"post_logout_redirect_uris":["https://localhost:3457/goodbye"],"frontchannel_logout_session_required":false,"registration_access_token":"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDozNDU3Iiwic3ViIjoiN2Y4MWY3MGM3YzMwNmU5NmZkYjUxODFmNGMxZDcyMjIiLCJhdWQiOiI3ZjgxZjcwYzdjMzA2ZTk2ZmRiNTE4MWY0YzFkNzIyMiJ9.fT9uzCeGQXJHV-z1X9Fh2DHavvFpNNSBl7p_XiadxdGcX_TkDI_NAtuwX02bRugas6OToWuISHDEkR2PbVaEp7enhnO_bNuDko8AlmfdXmcNCc8a_VXBX_pDl6oiyQQRBqDDOff1fvvqvaJO1n3ssQMJnwJFHqQ75xLaodCPRiP5z3wfLliLJAYZaQVdOf1sBBpd8pxfSGnN7XkNyit71XlWlJR1Z_TaCYVF0EtZyT56rDT8_SFzvLlGpBU8JsmzoETozNKQnj4Zo9Uks_FtKIFNFUm2Oa3KMynTYHJJWxZ92c0oU4EUN6sjioHjVMOPSAkyeM6_Fv6LY-76kFvUpw","registration_client_uri":"https://localhost:3457/register/7f81f70c7c306e96fdb5181f4c1d7222","client_id_issued_at":1489775058,"client_secret_expires_at":0}} \ No newline at end of file diff --git a/test/resources/accounts/errortests/.acl b/test/resources/accounts/errortests/.acl new file mode 100644 index 000000000..f7be11c66 --- /dev/null +++ b/test/resources/accounts/errortests/.acl @@ -0,0 +1,16 @@ +@prefix acl: . + +<#owner> + a acl:Authorization; + + acl:agent ; + + # Set the access to the root storage folder itself + acl:accessTo ; + + # All resources will inherit this authorization, by default + acl:defaultForNew ; + + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. diff --git a/test/resources/acl-tls/append-acl/abc.ttl b/test/resources/acl-tls/append-acl/abc.ttl new file mode 100644 index 000000000..5296a5255 --- /dev/null +++ b/test/resources/acl-tls/append-acl/abc.ttl @@ -0,0 +1 @@ + . diff --git a/test/resources/acl/append-acl/abc.ttl.acl b/test/resources/acl-tls/append-acl/abc.ttl.acl similarity index 100% rename from test/resources/acl/append-acl/abc.ttl.acl rename to test/resources/acl-tls/append-acl/abc.ttl.acl diff --git a/test/resources/acl-tls/append-acl/abc2.ttl b/test/resources/acl-tls/append-acl/abc2.ttl new file mode 100644 index 000000000..07eff8ea5 --- /dev/null +++ b/test/resources/acl-tls/append-acl/abc2.ttl @@ -0,0 +1 @@ + . diff --git a/test/resources/acl/append-acl/abc2.ttl.acl b/test/resources/acl-tls/append-acl/abc2.ttl.acl similarity index 100% rename from test/resources/acl/append-acl/abc2.ttl.acl rename to test/resources/acl-tls/append-acl/abc2.ttl.acl diff --git a/test/resources/acl/append-inherited/.acl b/test/resources/acl-tls/append-inherited/.acl similarity index 100% rename from test/resources/acl/append-inherited/.acl rename to test/resources/acl-tls/append-inherited/.acl diff --git a/test/resources/acl-tls/empty-acl/.acl b/test/resources/acl-tls/empty-acl/.acl new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/acl-tls/fake-account/.acl b/test/resources/acl-tls/fake-account/.acl new file mode 100644 index 000000000..f49950774 --- /dev/null +++ b/test/resources/acl-tls/fake-account/.acl @@ -0,0 +1,5 @@ +<#0> + a ; + <./> ; + ; + , . diff --git a/test/resources/acl-tls/fake-account/hello.html b/test/resources/acl-tls/fake-account/hello.html new file mode 100644 index 000000000..7fd820ca9 --- /dev/null +++ b/test/resources/acl-tls/fake-account/hello.html @@ -0,0 +1,9 @@ + + + + Hello + + +Hello + + \ No newline at end of file diff --git a/test/resources/acl-tls/no-acl/test-file.html b/test/resources/acl-tls/no-acl/test-file.html new file mode 100644 index 000000000..16b832e3f --- /dev/null +++ b/test/resources/acl-tls/no-acl/test-file.html @@ -0,0 +1 @@ +test-file.html \ No newline at end of file diff --git a/test/resources/acl/origin/.acl b/test/resources/acl-tls/origin/.acl similarity index 100% rename from test/resources/acl/origin/.acl rename to test/resources/acl-tls/origin/.acl diff --git a/test/resources/acl/owner-only/.acl b/test/resources/acl-tls/owner-only/.acl similarity index 100% rename from test/resources/acl/owner-only/.acl rename to test/resources/acl-tls/owner-only/.acl diff --git a/test/resources/acl/read-acl/.acl b/test/resources/acl-tls/read-acl/.acl similarity index 100% rename from test/resources/acl/read-acl/.acl rename to test/resources/acl-tls/read-acl/.acl diff --git a/test/resources/acl/write-acl/.acl b/test/resources/acl-tls/write-acl/.acl similarity index 100% rename from test/resources/acl/write-acl/.acl rename to test/resources/acl-tls/write-acl/.acl diff --git a/test/resources/acl-tls/write-acl/empty-acl/.acl b/test/resources/acl-tls/write-acl/empty-acl/.acl new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/sampleContainer2/example1.ttl b/test/resources/sampleContainer2/example1.ttl new file mode 100644 index 000000000..c2a488461 --- /dev/null +++ b/test/resources/sampleContainer2/example1.ttl @@ -0,0 +1,10 @@ +@prefix rdf: . +@prefix dc: . +@prefix ex: . + + + dc:title "RDF/XML Syntax Specification (Revised)" ; + ex:editor [ + ex:fullname "Dave Beckett"; + ex:homePage + ] . diff --git a/test/resources/sampleContainer2/example2.ttl b/test/resources/sampleContainer2/example2.ttl new file mode 100644 index 000000000..8259de95d --- /dev/null +++ b/test/resources/sampleContainer2/example2.ttl @@ -0,0 +1,7 @@ +@prefix : . +@prefix rdf: . +:a :b + [ rdf:first "apple"; + rdf:rest [ rdf:first "banana"; + rdf:rest rdf:nil ] + ] . diff --git a/test/account-manager.js b/test/unit/account-manager.js similarity index 56% rename from test/account-manager.js rename to test/unit/account-manager.js index 5facde2e6..fc7f57584 100644 --- a/test/account-manager.js +++ b/test/unit/account-manager.js @@ -1,22 +1,24 @@ 'use strict' const path = require('path') -const fs = require('fs-extra') const chai = require('chai') const expect = chai.expect const sinon = require('sinon') const sinonChai = require('sinon-chai') chai.use(sinonChai) +chai.use(require('dirty-chai')) chai.should() const rdf = require('rdflib') -const LDP = require('../lib/ldp') -const SolidHost = require('../lib/models/solid-host') -const AccountManager = require('../lib/models/account-manager') -const UserAccount = require('../lib/models/user-account') -const WebIdTlsCertificate = require('../lib/models/webid-tls-certificate') +const ns = require('solid-namespace')(rdf) +const LDP = require('../../lib/ldp') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') +const UserAccount = require('../../lib/models/user-account') +const TokenService = require('../../lib/models/token-service') +const WebIdTlsCertificate = require('../../lib/models/webid-tls-certificate') -const testAccountsDir = path.join(__dirname, 'resources', 'accounts') +const testAccountsDir = path.join(__dirname, '../resources/accounts') var host @@ -29,10 +31,11 @@ describe('AccountManager', () => { it('should init with passed in options', () => { let config = { host, - authMethod: 'tls', + authMethod: 'oidc', multiUser: true, store: {}, - emailService: {} + emailService: {}, + tokenService: {} } let mgr = AccountManager.from(config) @@ -41,6 +44,7 @@ describe('AccountManager', () => { expect(mgr.multiUser).to.equal(config.multiUser) expect(mgr.store).to.equal(config.store) expect(mgr.emailService).to.equal(config.emailService) + expect(mgr.tokenService).to.equal(config.tokenService) }) it('should error if no host param is passed in', () => { @@ -118,65 +122,6 @@ describe('AccountManager', () => { }) }) - describe('accountExists()', () => { - let host = SolidHost.from({ serverUri: 'https://localhost' }) - - describe('in multi user mode', () => { - let multiUser = true - let store = new LDP({ root: testAccountsDir, idp: multiUser }) - let options = { multiUser, store, host } - let accountManager = AccountManager.from(options) - - it('resolves to true if a directory for the account exists in root', () => { - // Note: test/resources/accounts/tim.localhost/ exists in this repo - return accountManager.accountExists('tim') - .then(exists => { - expect(exists).to.be.true - }) - }) - - it('resolves to false if a directory for the account does not exist', () => { - // Note: test/resources/accounts/alice.localhost/ does NOT exist - return accountManager.accountExists('alice') - .then(exists => { - expect(exists).to.be.false - }) - }) - }) - - describe('in single user mode', () => { - let multiUser = false - - it('resolves to true if root .acl exists in root storage', () => { - let store = new LDP({ - root: path.join(testAccountsDir, 'tim.localhost'), - idp: multiUser - }) - let options = { multiUser, store, host } - let accountManager = AccountManager.from(options) - - return accountManager.accountExists() - .then(exists => { - expect(exists).to.be.true - }) - }) - - it('resolves to false if root .acl does not exist in root storage', () => { - let store = new LDP({ - root: testAccountsDir, - idp: multiUser - }) - let options = { multiUser, store, host } - let accountManager = AccountManager.from(options) - - return accountManager.accountExists() - .then(exists => { - expect(exists).to.be.false - }) - }) - }) - }) - describe('userAccountFrom()', () => { describe('in multi user mode', () => { let multiUser = true @@ -326,42 +271,210 @@ describe('AccountManager', () => { }) }) - describe('createAccountFor()', () => { - after(() => { - fs.removeSync(path.join(__dirname, 'resources', 'accounts', 'alice.example.com')) + describe('rootAclFor()', () => { + it('should return the server root .acl in single user mode', () => { + let store = new LDP({ suffixAcl: '.acl', idp: false }) + let options = { host, multiUser: false, store } + let accountManager = AccountManager.from(options) + + let userAccount = UserAccount.from({ username: 'alice' }) + + let rootAclUri = accountManager.rootAclFor(userAccount) + + expect(rootAclUri).to.equal('https://example.com/.acl') }) - it('should create an account directory', () => { - let multiUser = true - let accountTemplatePath = path.join(__dirname, '../default-account-template') - let store = new LDP({ root: testAccountsDir, idp: multiUser }) - let options = { host, multiUser, store, accountTemplatePath } + it('should return the profile root .acl in multi user mode', () => { + let store = new LDP({ suffixAcl: '.acl', idp: true }) + let options = { host, multiUser: true, store } let accountManager = AccountManager.from(options) - let userData = { - username: 'alice', - email: 'alice@example.com', - name: 'Alice Q.' + let userAccount = UserAccount.from({ username: 'alice' }) + + let rootAclUri = accountManager.rootAclFor(userAccount) + + expect(rootAclUri).to.equal('https://alice.example.com/.acl') + }) + }) + + describe('loadAccountRecoveryEmail()', () => { + it('parses and returns the agent mailto from the root acl', () => { + let userAccount = UserAccount.from({ username: 'alice' }) + + let rootAclGraph = rdf.graph() + rootAclGraph.add( + rdf.namedNode('https://alice.example.com/.acl#owner'), + ns.acl('agent'), + rdf.namedNode('mailto:alice@example.com') + ) + + let store = { + suffixAcl: '.acl', + getGraph: sinon.stub().resolves(rootAclGraph) } - let userAccount = accountManager.userAccountFrom(userData) - let accountDir = accountManager.accountDirFor('alice') + let options = { host, multiUser: true, store } + let accountManager = AccountManager.from(options) - return accountManager.createAccountFor(userAccount) - .then(() => { - return accountManager.accountExists('alice') + return accountManager.loadAccountRecoveryEmail(userAccount) + .then(recoveryEmail => { + expect(recoveryEmail).to.equal('alice@example.com') }) - .then(found => { - expect(found).to.be.true + }) + + it('should return undefined when agent mailto is missing', () => { + let userAccount = UserAccount.from({ username: 'alice' }) + + let emptyGraph = rdf.graph() + + let store = { + suffixAcl: '.acl', + getGraph: sinon.stub().resolves(emptyGraph) + } + + let options = { host, multiUser: true, store } + let accountManager = AccountManager.from(options) + + return accountManager.loadAccountRecoveryEmail(userAccount) + .then(recoveryEmail => { + expect(recoveryEmail).to.be.undefined() }) + }) + }) + + describe('passwordResetUrl()', () => { + it('should return a token reset validation url', () => { + let tokenService = new TokenService() + let options = { host, multiUser: true, tokenService } + + let accountManager = AccountManager.from(options) + + let returnToUrl = 'https://example.com/resource' + let token = '123' + + let resetUrl = accountManager.passwordResetUrl(token, returnToUrl) + + let expectedUri = 'https://example.com/account/password/change?' + + 'token=123&returnToUrl=' + returnToUrl + + expect(resetUrl).to.equal(expectedUri) + }) + }) + + describe('generateResetToken()', () => { + it('should generate and store an expiring reset token', () => { + let tokenService = new TokenService() + let options = { host, tokenService } + + let accountManager = AccountManager.from(options) + + let aliceWebId = 'https://alice.example.com/#me' + let userAccount = { + webId: aliceWebId + } + + let token = accountManager.generateResetToken(userAccount) + + let tokenValue = accountManager.tokenService.verify(token) + + expect(tokenValue.webId).to.equal(aliceWebId) + expect(tokenValue).to.have.property('exp') + }) + }) + + describe('sendPasswordResetEmail()', () => { + it('should compose and send a password reset email', () => { + let resetToken = '1234' + let tokenService = { + generate: sinon.stub().returns(resetToken) + } + + let emailService = { + sendWithTemplate: sinon.stub().resolves() + } + + let aliceWebId = 'https://alice.example.com/#me' + let userAccount = { + webId: aliceWebId, + email: 'alice@example.com' + } + let returnToUrl = 'https://example.com/resource' + + let options = { host, tokenService, emailService } + let accountManager = AccountManager.from(options) + + accountManager.passwordResetUrl = sinon.stub().returns('reset url') + + let expectedEmailData = { + to: 'alice@example.com', + webId: aliceWebId, + resetUrl: 'reset url' + } + + return accountManager.sendPasswordResetEmail(userAccount, returnToUrl) .then(() => { - let profile = fs.readFileSync(path.join(accountDir, '/profile/card'), 'utf8') - expect(profile).to.include('"Alice Q."') + expect(accountManager.passwordResetUrl) + .to.have.been.calledWith(resetToken, returnToUrl) + expect(emailService.sendWithTemplate) + .to.have.been.calledWith('reset-password', expectedEmailData) + }) + }) - let rootAcl = fs.readFileSync(path.join(accountDir, '.acl'), 'utf8') - expect(rootAcl).to.include('') + it('should reject if no email service is set up', done => { + let aliceWebId = 'https://alice.example.com/#me' + let userAccount = { + webId: aliceWebId, + email: 'alice@example.com' + } + let returnToUrl = 'https://example.com/resource' + let options = { host } + let accountManager = AccountManager.from(options) + + accountManager.sendPasswordResetEmail(userAccount, returnToUrl) + .catch(error => { + expect(error.message).to.equal('Email service is not set up') + done() }) }) + + it('should reject if no user email is provided', done => { + let aliceWebId = 'https://alice.example.com/#me' + let userAccount = { + webId: aliceWebId + } + let returnToUrl = 'https://example.com/resource' + let emailService = {} + let options = { host, emailService } + + let accountManager = AccountManager.from(options) + + accountManager.sendPasswordResetEmail(userAccount, returnToUrl) + .catch(error => { + expect(error.message).to.equal('Account recovery email has not been provided') + done() + }) + }) + }) + + describe('externalAccount()', () => { + it('should return true if account is a subdomain of the local server url', () => { + let options = { host } + + let accountManager = AccountManager.from(options) + + let webId = 'https://alice.example.com/#me' + + expect(accountManager.externalAccount(webId)).to.be.false() + }) + + it('should return false if account does not match the local server url', () => { + let options = { host } + + let accountManager = AccountManager.from(options) + + let webId = 'https://alice.databox.me/#me' + + expect(accountManager.externalAccount(webId)).to.be.true() + }) }) }) diff --git a/test/account-template.js b/test/unit/account-template.js similarity index 56% rename from test/account-template.js rename to test/unit/account-template.js index cb99729d4..eb1d653b0 100644 --- a/test/account-template.js +++ b/test/unit/account-template.js @@ -1,38 +1,15 @@ 'use strict' -const path = require('path') -const fs = require('fs-extra') const chai = require('chai') const expect = chai.expect const sinonChai = require('sinon-chai') chai.use(sinonChai) chai.should() -const AccountTemplate = require('../lib/models/account-template') -const UserAccount = require('../lib/models/user-account') - -const templatePath = path.join(__dirname, '../default-account-template') -const accountPath = path.join(__dirname, 'resources', 'new-account') +const AccountTemplate = require('../../lib/models/account-template') +const UserAccount = require('../../lib/models/user-account') describe('AccountTemplate', () => { - beforeEach(() => { - fs.removeSync(accountPath) - }) - - afterEach(() => { - fs.removeSync(accountPath) - }) - - describe('copy()', () => { - it('should copy a directory', () => { - return AccountTemplate.copyTemplateDir(templatePath, accountPath) - .then(() => { - let rootAcl = fs.readFileSync(path.join(accountPath, '.acl')) - expect(rootAcl).to.exist - }) - }) - }) - describe('isTemplate()', () => { let template = new AccountTemplate() @@ -79,28 +56,4 @@ describe('AccountTemplate', () => { expect(substitutions.webId).to.equal('https://alice.example.com/profile/card#me') }) }) - - describe('processAccount()', () => { - it('should process all the files in an account', () => { - let substitutions = { - webId: 'https://alice.example.com/#me', - email: 'alice@example.com', - name: 'Alice Q.' - } - let template = new AccountTemplate({ substitutions }) - - return AccountTemplate.copyTemplateDir(templatePath, accountPath) - .then(() => { - return template.processAccount(accountPath) - }) - .then(() => { - let profile = fs.readFileSync(path.join(accountPath, '/profile/card'), 'utf8') - expect(profile).to.include('"Alice Q."') - - let rootAcl = fs.readFileSync(path.join(accountPath, '.acl'), 'utf8') - expect(rootAcl).to.include('') - }) - }) - }) }) diff --git a/test/acl-checker.js b/test/unit/acl-checker.js similarity index 55% rename from test/acl-checker.js rename to test/unit/acl-checker.js index fd80d20b2..c8711f87e 100644 --- a/test/acl-checker.js +++ b/test/unit/acl-checker.js @@ -1,7 +1,15 @@ 'use strict' const proxyquire = require('proxyquire') -const assert = require('chai').assert -const debug = require('../lib/debug').ACL +const chai = require('chai') +const { assert, expect } = chai +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const sinon = require('sinon') +const sinonChai = require('sinon-chai') +chai.use(sinonChai) +chai.should() +const debug = require('../../lib/debug').ACL +const { userIdFromRequest } = require('../../lib/handlers/allow') class PermissionSetAlwaysGrant { checkAccess () { @@ -19,9 +27,47 @@ class PermissionSetAlwaysError { } } +describe('Allow handler', () => { + let req + let aliceWebId = 'https://alice.example.com/#me' + + beforeEach(() => { + req = { app: { locals: {} }, session: {} } + }) + + describe('userIdFromRequest()', () => { + it('should first look in session.userId', () => { + req.session.userId = aliceWebId + + let userId = userIdFromRequest(req) + + expect(userId).to.equal(aliceWebId) + }) + + it('should use webIdFromClaims() if applicable', () => { + req.app.locals.authMethod = 'oidc' + req.claims = {} + + let webIdFromClaims = sinon.stub().returns(aliceWebId) + req.app.locals.oidc = { webIdFromClaims } + + let userId = userIdFromRequest(req) + + expect(userId).to.equal(aliceWebId) + expect(webIdFromClaims).to.have.been.calledWith(req.claims) + }) + + it('should return falsy if all else fails', () => { + let userId = userIdFromRequest(req) + + expect(userId).to.not.be.ok() + }) + }) +}) + describe('ACLChecker unit test', () => { it('should callback with null on grant success', done => { - let ACLChecker = proxyquire('../lib/acl-checker', { + let ACLChecker = proxyquire('../../lib/acl-checker', { 'solid-permissions': { PermissionSet: PermissionSetAlwaysGrant } }) let graph = {} @@ -35,7 +81,7 @@ describe('ACLChecker unit test', () => { }) }) it('should callback with error on grant failure', done => { - let ACLChecker = proxyquire('../lib/acl-checker', { + let ACLChecker = proxyquire('../../lib/acl-checker', { 'solid-permissions': { PermissionSet: PermissionSetNeverGrant } }) let graph = {} @@ -49,7 +95,7 @@ describe('ACLChecker unit test', () => { }) }) it('should callback with error on grant error', done => { - let ACLChecker = proxyquire('../lib/acl-checker', { + let ACLChecker = proxyquire('../../lib/acl-checker', { 'solid-permissions': { PermissionSet: PermissionSetAlwaysError } }) let graph = {} diff --git a/test/add-cert-request.js b/test/unit/add-cert-request.js similarity index 85% rename from test/add-cert-request.js rename to test/unit/add-cert-request.js index d13c2e829..05d907859 100644 --- a/test/add-cert-request.js +++ b/test/unit/add-cert-request.js @@ -12,13 +12,13 @@ chai.use(sinonChai) chai.should() const HttpMocks = require('node-mocks-http') -const SolidHost = require('../lib/models/solid-host') -const AccountManager = require('../lib/models/account-manager') -const AddCertificateRequest = require('../lib/requests/add-cert-request') -const WebIdTlsCertificate = require('../lib/models/webid-tls-certificate') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') +const AddCertificateRequest = require('../../lib/requests/add-cert-request') +const WebIdTlsCertificate = require('../../lib/models/webid-tls-certificate') const exampleSpkac = fs.readFileSync( - path.join(__dirname, './resources/example_spkac.cnf'), 'utf8' + path.join(__dirname, '../resources/example_spkac.cnf'), 'utf8' ) var host @@ -31,7 +31,7 @@ describe('AddCertificateRequest', () => { describe('fromParams()', () => { it('should throw a 401 error if session.userId is missing', () => { let multiUser = true - let options = { host, multiUser, authMethod: 'tls' } + let options = { host, multiUser, authMethod: 'oidc' } let accountManager = AccountManager.from(options) let req = { @@ -52,7 +52,7 @@ describe('AddCertificateRequest', () => { let multiUser = true it('should call certificate.generateCertificate()', () => { - let options = { host, multiUser, authMethod: 'tls' } + let options = { host, multiUser, authMethod: 'oidc' } let accountManager = AccountManager.from(options) let req = { @@ -81,7 +81,7 @@ describe('AddCertificateRequest', () => { let multiUser = true it('should add certificate data to a graph', () => { - let options = { host, multiUser, authMethod: 'tls' } + let options = { host, multiUser, authMethod: 'oidc' } let accountManager = AccountManager.from(options) let userData = { username: 'alice' } @@ -111,7 +111,7 @@ describe('AddCertificateRequest', () => { expect(graph.anyStatementMatching(key, ns.cert('exponent'))) .to.exist }) - }) + }).timeout(3000) }) }) diff --git a/test/unit/auth-handlers.js b/test/unit/auth-handlers.js new file mode 100644 index 000000000..a55912fb2 --- /dev/null +++ b/test/unit/auth-handlers.js @@ -0,0 +1,103 @@ +'use strict' +const chai = require('chai') +const sinon = require('sinon') +const { expect } = chai +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.should() + +const Auth = require('../../lib/api/authn') + +describe('OIDC Handler', () => { + describe('setAuthenticateHeader()', () => { + let res, req + + beforeEach(() => { + req = { + app: { + locals: { host: { serverUri: 'https://example.com' } } + }, + get: sinon.stub() + } + res = { set: sinon.stub() } + }) + + it('should set the WWW-Authenticate header with error params', () => { + let error = { + error: 'invalid_token', + error_description: 'Invalid token', + error_uri: 'https://example.com/errors/token' + } + + Auth.oidc.setAuthenticateHeader(req, res, error) + + expect(res.set).to.be.calledWith( + 'WWW-Authenticate', + 'Bearer realm="https://example.com", scope="openid", error="invalid_token", error_description="Invalid token", error_uri="https://example.com/errors/token"' + ) + }) + + it('should set WWW-Authenticate with no error_description if none given', () => { + let error = {} + + Auth.oidc.setAuthenticateHeader(req, res, error) + + expect(res.set).to.be.calledWith( + 'WWW-Authenticate', + 'Bearer realm="https://example.com", scope="openid"' + ) + }) + }) + + describe('isEmptyToken()', () => { + let req + + beforeEach(() => { + req = { get: sinon.stub() } + }) + + it('should be true for empty access token', () => { + req.get.withArgs('Authorization').returns('Bearer ') + + expect(Auth.oidc.isEmptyToken(req)).to.be.true() + + req.get.withArgs('Authorization').returns('Bearer') + + expect(Auth.oidc.isEmptyToken(req)).to.be.true() + }) + + it('should be false when access token is present', () => { + req.get.withArgs('Authorization').returns('Bearer token123') + + expect(Auth.oidc.isEmptyToken(req)).to.be.false() + }) + + it('should be false when no authorization header is present', () => { + expect(Auth.oidc.isEmptyToken(req)).to.be.false() + }) + }) +}) + +describe('WebID-TLS Handler', () => { + describe('setAuthenticateHeader()', () => { + let res, req + + beforeEach(() => { + req = { + app: { + locals: { host: { serverUri: 'https://example.com' } } + } + } + res = { set: sinon.stub() } + }) + + it('should set the WWW-Authenticate header', () => { + Auth.tls.setAuthenticateHeader(req, res) + + expect(res.set).to.be.calledWith( + 'WWW-Authenticate', + 'WebID-TLS realm="https://example.com"' + ) + }) + }) +}) diff --git a/test/unit/auth-request.js b/test/unit/auth-request.js new file mode 100644 index 000000000..a434d83bb --- /dev/null +++ b/test/unit/auth-request.js @@ -0,0 +1,101 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +// const sinon = require('sinon') +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.should() +// const HttpMocks = require('node-mocks-http') +const url = require('url') + +const AuthRequest = require('../../lib/requests/auth-request') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') +const UserAccount = require('../../lib/models/user-account') + +describe('AuthRequest', () => { + function testAuthQueryParams () { + let body = {} + body['response_type'] = 'code' + body['scope'] = 'openid' + body['client_id'] = 'client1' + body['redirect_uri'] = 'https://redirect.example.com/' + body['state'] = '1234' + body['nonce'] = '5678' + body['display'] = 'page' + + return body + } + + const host = SolidHost.from({ serverUri: 'https://localhost:8443' }) + const accountManager = AccountManager.from({ host }) + + describe('extractAuthParams()', () => { + it('should initialize the auth url query object from params', () => { + let body = testAuthQueryParams() + body['other_key'] = 'whatever' + let req = { body, method: 'POST' } + + let extracted = AuthRequest.extractAuthParams(req) + + for (let param of AuthRequest.AUTH_QUERY_PARAMS) { + expect(extracted[param]).to.equal(body[param]) + } + + // make sure *only* the listed params were copied + expect(extracted['other_key']).to.not.exist() + }) + + it('should return empty params with no request body present', () => { + let req = { method: 'POST' } + + expect(AuthRequest.extractAuthParams(req)).to.eql({}) + }) + }) + + describe('authorizeUrl()', () => { + it('should return an /authorize url', () => { + let request = new AuthRequest({ accountManager }) + + let authUrl = request.authorizeUrl() + + expect(authUrl.startsWith('https://localhost:8443/authorize')).to.be.true() + }) + + it('should pass through relevant auth query params from request body', () => { + let body = testAuthQueryParams() + let req = { body, method: 'POST' } + + let request = new AuthRequest({ accountManager }) + request.authQueryParams = AuthRequest.extractAuthParams(req) + + let authUrl = request.authorizeUrl() + + let parseQueryString = true + let parsedUrl = url.parse(authUrl, parseQueryString) + + for (let param in body) { + expect(body[param]).to.equal(parsedUrl.query[param]) + } + }) + }) + + describe('initUserSession()', () => { + it('should initialize the request session', () => { + let webId = 'https://alice.example.com/#me' + let alice = UserAccount.from({ username: 'alice', webId }) + let session = {} + + let request = new AuthRequest({ session }) + + request.initUserSession(alice) + + expect(request.session.userId).to.equal(webId) + expect(request.session.identified).to.be.true() + let subject = request.session.subject + expect(subject['_id']).to.equal(webId) + }) + }) +}) + diff --git a/test/unit/authenticator.js b/test/unit/authenticator.js new file mode 100644 index 000000000..83197675c --- /dev/null +++ b/test/unit/authenticator.js @@ -0,0 +1,34 @@ +'use strict' +const chai = require('chai') +const { expect } = chai +chai.use(require('chai-as-promised')) +chai.should() + +const { Authenticator } = require('../../lib/models/authenticator') + +describe('Authenticator', () => { + describe('constructor()', () => { + it('should initialize the accountManager property', () => { + let accountManager = {} + let auth = new Authenticator({ accountManager }) + + expect(auth.accountManager).to.equal(accountManager) + }) + }) + + describe('fromParams()', () => { + it('should throw an abstract method error', () => { + expect(() => Authenticator.fromParams()) + .to.throw(/Must override method/) + }) + }) + + describe('findValidUser()', () => { + it('should throw an abstract method error', () => { + let auth = new Authenticator({}) + + expect(() => auth.findValidUser()) + .to.throw(/Must override method/) + }) + }) +}) diff --git a/test/unit/create-account-request.js b/test/unit/create-account-request.js new file mode 100644 index 000000000..e73c0244b --- /dev/null +++ b/test/unit/create-account-request.js @@ -0,0 +1,221 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +const sinonChai = require('sinon-chai') +chai.use(sinonChai) +chai.should() +const HttpMocks = require('node-mocks-http') + +const LDP = require('../../lib/ldp') +const AccountManager = require('../../lib/models/account-manager') +const SolidHost = require('../../lib/models/solid-host') +const defaults = require('../../config/defaults') +const { CreateAccountRequest } = require('../../lib/requests/create-account-request') + +describe('CreateAccountRequest', () => { + let host, store, accountManager + let session, res + + beforeEach(() => { + host = SolidHost.from({ serverUri: 'https://example.com' }) + store = new LDP() + accountManager = AccountManager.from({ host, store }) + + session = {} + res = HttpMocks.createResponse() + }) + + describe('constructor()', () => { + it('should create an instance with the given config', () => { + let aliceData = { username: 'alice' } + let userAccount = accountManager.userAccountFrom(aliceData) + + let options = { accountManager, userAccount, session, response: res } + let request = new CreateAccountRequest(options) + + expect(request.accountManager).to.equal(accountManager) + expect(request.userAccount).to.equal(userAccount) + expect(request.session).to.equal(session) + expect(request.response).to.equal(res) + }) + }) + + describe('fromParams()', () => { + it('should create subclass depending on authMethod', () => { + let request, aliceData, req + + aliceData = { username: 'alice' } + req = HttpMocks.createRequest({ + app: { locals: { accountManager } }, body: aliceData, session + }) + req.app.locals.authMethod = 'tls' + + request = CreateAccountRequest.fromParams(req, res, accountManager) + expect(request).to.respondTo('generateTlsCertificate') + + aliceData = { username: 'alice', password: '12345' } + req = HttpMocks.createRequest({ + app: { locals: { accountManager, oidc: {} } }, body: aliceData, session + }) + req.app.locals.authMethod = 'oidc' + request = CreateAccountRequest.fromParams(req, res, accountManager) + expect(request).to.not.respondTo('generateTlsCertificate') + }) + }) + + describe('createAccount()', () => { + it('should return a 400 error if account already exists', done => { + let accountManager = AccountManager.from({ host }) + let locals = { authMethod: defaults.auth, accountManager, oidc: { users: {} } } + let aliceData = { + username: 'alice', password: '1234' + } + let req = HttpMocks.createRequest({ app: { locals }, body: aliceData }) + + let request = CreateAccountRequest.fromParams(req, res) + + accountManager.accountExists = sinon.stub().returns(Promise.resolve(true)) + + request.createAccount() + .catch(err => { + expect(err.status).to.equal(400) + done() + }) + }) + }) +}) + +describe('CreateOidcAccountRequest', () => { + let authMethod = 'oidc' + let host, store + let session, res + + beforeEach(() => { + host = SolidHost.from({ serverUri: 'https://example.com' }) + store = new LDP() + session = {} + res = HttpMocks.createResponse() + }) + + describe('fromParams()', () => { + it('should create an instance with the given config', () => { + let accountManager = AccountManager.from({ host, store }) + let aliceData = { username: 'alice', password: '123' } + + let userStore = {} + let req = HttpMocks.createRequest({ + app: { + locals: { authMethod, oidc: { users: userStore }, accountManager } + }, + body: aliceData, + session + }) + + let request = CreateAccountRequest.fromParams(req, res) + + expect(request.accountManager).to.equal(accountManager) + expect(request.userAccount.username).to.equal('alice') + expect(request.session).to.equal(session) + expect(request.response).to.equal(res) + expect(request.password).to.equal(aliceData.password) + expect(request.userStore).to.equal(userStore) + }) + }) + + describe('saveCredentialsFor()', () => { + it('should create a new user in the user store', () => { + let accountManager = AccountManager.from({ host, store }) + let password = '12345' + let aliceData = { username: 'alice', password } + let userStore = { + createUser: (userAccount, password) => { return Promise.resolve() } + } + let createUserSpy = sinon.spy(userStore, 'createUser') + let req = HttpMocks.createRequest({ + app: { locals: { authMethod, oidc: { users: userStore }, accountManager } }, + body: aliceData, + session + }) + + let request = CreateAccountRequest.fromParams(req, res) + let userAccount = request.userAccount + + return request.saveCredentialsFor(userAccount) + .then(() => { + expect(createUserSpy).to.have.been.calledWith(userAccount, password) + }) + }) + }) + + describe('sendResponse()', () => { + it('should respond with a 302 Redirect', () => { + let accountManager = AccountManager.from({ host, store }) + let aliceData = { username: 'alice', password: '12345' } + let req = HttpMocks.createRequest({ + app: { locals: { authMethod, oidc: {}, accountManager } }, + body: aliceData, + session + }) + let alice = accountManager.userAccountFrom(aliceData) + + let request = CreateAccountRequest.fromParams(req, res) + + let result = request.sendResponse(alice) + expect(request.response.statusCode).to.equal(302) + expect(result.username).to.equal('alice') + }) + }) +}) + +describe('CreateTlsAccountRequest', () => { + let authMethod = 'tls' + let host, store + let session, res + + beforeEach(() => { + host = SolidHost.from({ serverUri: 'https://example.com' }) + store = new LDP() + session = {} + res = HttpMocks.createResponse() + }) + + describe('fromParams()', () => { + it('should create an instance with the given config', () => { + let accountManager = AccountManager.from({ host, store }) + let aliceData = { username: 'alice' } + let req = HttpMocks.createRequest({ + app: { locals: { authMethod, accountManager } }, body: aliceData, session + }) + + let request = CreateAccountRequest.fromParams(req, res) + + expect(request.accountManager).to.equal(accountManager) + expect(request.userAccount.username).to.equal('alice') + expect(request.session).to.equal(session) + expect(request.response).to.equal(res) + expect(request.spkac).to.equal(aliceData.spkac) + }) + }) + + describe('saveCredentialsFor()', () => { + it('should call generateTlsCertificate()', () => { + let accountManager = AccountManager.from({ host, store }) + let aliceData = { username: 'alice' } + let req = HttpMocks.createRequest({ + app: { locals: { authMethod, accountManager } }, body: aliceData, session + }) + + let request = CreateAccountRequest.fromParams(req, res) + let userAccount = accountManager.userAccountFrom(aliceData) + + let generateTlsCertificate = sinon.spy(request, 'generateTlsCertificate') + + return request.saveCredentialsFor(userAccount) + .then(() => { + expect(generateTlsCertificate).to.have.been.calledWith(userAccount) + }) + }) + }) +}) diff --git a/test/email-service.js b/test/unit/email-service.js similarity index 95% rename from test/email-service.js rename to test/unit/email-service.js index dc88f54be..97de0ca80 100644 --- a/test/email-service.js +++ b/test/unit/email-service.js @@ -1,4 +1,4 @@ -const EmailService = require('../lib/models/email-service') +const EmailService = require('../../lib/models/email-service') const path = require('path') const sinon = require('sinon') const chai = require('chai') @@ -7,12 +7,12 @@ const sinonChai = require('sinon-chai') chai.use(sinonChai) chai.should() -const templatePath = path.join(__dirname, '../default-email-templates') +const templatePath = path.join(__dirname, '../../default-templates/emails') describe('Email Service', function () { describe('EmailService constructor', () => { it('should set up a nodemailer instance', () => { - let templatePath = '../config/email-templates' + let templatePath = '../../config/email-templates' let config = { host: 'smtp.gmail.com', auth: { @@ -91,7 +91,7 @@ describe('Email Service', function () { describe('templatePathFor()', () => { it('should compose filename based on base path and template name', () => { let config = { host: 'databox.me', auth: {} } - let templatePath = '../config/email-templates' + let templatePath = '../../config/email-templates' let emailService = new EmailService(templatePath, config) let templateFile = emailService.templatePathFor('welcome') diff --git a/test/email-welcome.js b/test/unit/email-welcome.js similarity index 87% rename from test/email-welcome.js rename to test/unit/email-welcome.js index cb58286a9..e81769b70 100644 --- a/test/email-welcome.js +++ b/test/unit/email-welcome.js @@ -8,11 +8,11 @@ const sinonChai = require('sinon-chai') chai.use(sinonChai) chai.should() -const SolidHost = require('../lib/models/solid-host') -const AccountManager = require('../lib/models/account-manager') -const EmailService = require('../lib/models/email-service') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') +const EmailService = require('../../lib/models/email-service') -const templatePath = path.join(__dirname, '../default-email-templates') +const templatePath = path.join(__dirname, '../../default-templates/emails') var host, accountManager, emailService @@ -25,7 +25,7 @@ beforeEach(() => { let mgrConfig = { host, emailService, - authMethod: 'tls', + authMethod: 'oidc', multiUser: true } accountManager = AccountManager.from(mgrConfig) diff --git a/test/unit/error-pages.js b/test/unit/error-pages.js new file mode 100644 index 000000000..3444dcd1e --- /dev/null +++ b/test/unit/error-pages.js @@ -0,0 +1,127 @@ +'use strict' +const chai = require('chai') +const sinon = require('sinon') +const { expect } = chai +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.should() + +const errorPages = require('../../lib/handlers/error-pages') + +describe('handlers/error-pages', () => { + describe('handler()', () => { + it('should use the custom error handler if available', () => { + let ldp = { errorHandler: sinon.stub() } + let req = { app: { locals: { ldp } } } + let res = { status: sinon.stub(), send: sinon.stub() } + let err = {} + let next = {} + + errorPages.handler(err, req, res, next) + + expect(ldp.errorHandler).to.have.been.calledWith(err, req, res, next) + + expect(res.status).to.not.have.been.called() + expect(res.send).to.not.have.been.called() + }) + + it('defaults to status code 500 if none is specified in the error', () => { + let ldp = { noErrorPages: true } + let req = { app: { locals: { ldp } } } + let res = { status: sinon.stub(), send: sinon.stub() } + let err = { message: 'Unspecified error' } + let next = {} + + errorPages.handler(err, req, res, next) + + expect(res.status).to.have.been.calledWith(500) + expect(res.send).to.have.been.calledWith('Unspecified error\n') + }) + }) + + describe('requiresSelectProvider()', () => { + it('should only apply to 401 error codes', () => { + let authMethod = 'oidc' + let req = { accepts: sinon.stub().withArgs('text/html').returns(true) } + + expect(errorPages.requiresSelectProvider(authMethod, 404, req)) + .to.equal(false) + expect(errorPages.requiresSelectProvider(authMethod, 401, req)) + .to.equal(true) + }) + + it('should only apply to oidc auth method', () => { + let statusCode = 401 + let req = { accepts: sinon.stub().withArgs('text/html').returns(true) } + + expect(errorPages.requiresSelectProvider('tls', statusCode, req)) + .to.equal(false) + expect(errorPages.requiresSelectProvider('oidc', statusCode, req)) + .to.equal(true) + }) + + it('should only apply to html requests', () => { + let authMethod = 'oidc' + let statusCode = 401 + let htmlReq = { accepts: sinon.stub().withArgs('text/html').returns(true) } + let nonHtmlReq = { accepts: sinon.stub().withArgs('text/html').returns(false) } + + expect(errorPages.requiresSelectProvider(authMethod, statusCode, nonHtmlReq)) + .to.equal(false) + expect(errorPages.requiresSelectProvider(authMethod, statusCode, htmlReq)) + .to.equal(true) + }) + }) + + describe('sendErrorResponse()', () => { + it('should send http status code and error message', () => { + let statusCode = 404 + let error = { + message: 'Error description' + } + let res = { + status: sinon.stub(), + send: sinon.stub() + } + + errorPages.sendErrorResponse(statusCode, res, error) + + expect(res.status).to.have.been.calledWith(404) + expect(res.send).to.have.been.calledWith('Error description\n') + }) + }) + + describe('setAuthenticateHeader()', () => { + it('should do nothing for a non-implemented auth method', () => { + let err = {} + let req = { + app: { locals: { authMethod: null } } + } + let res = { + set: sinon.stub() + } + + errorPages.setAuthenticateHeader(req, res, err) + + expect(res.set).to.not.have.been.called() + }) + }) + + describe('sendErrorPage()', () => { + it('falls back the default sendErrorResponse if no page is found', () => { + let statusCode = 400 + let res = { + status: sinon.stub(), + send: sinon.stub() + } + let err = { message: 'Error description' } + let ldp = { errorPages: './' } + + return errorPages.sendErrorPage(statusCode, res, err, ldp) + .then(() => { + expect(res.status).to.have.been.calledWith(400) + expect(res.send).to.have.been.calledWith('Error description\n') + }) + }) + }) +}) diff --git a/test/unit/login-request.js b/test/unit/login-request.js new file mode 100644 index 000000000..b1a585af2 --- /dev/null +++ b/test/unit/login-request.js @@ -0,0 +1,238 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.should() +const HttpMocks = require('node-mocks-http') + +const AuthRequest = require('../../lib/requests/auth-request') +const { LoginRequest } = require('../../lib/requests/login-request') + +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') + +const mockUserStore = { + findUser: () => { return Promise.resolve(true) }, + matchPassword: (user, password) => { return Promise.resolve(user) } +} + +const authMethod = 'oidc' +const host = SolidHost.from({ serverUri: 'https://localhost:8443' }) +const accountManager = AccountManager.from({ host, authMethod }) +const localAuth = { password: true, tls: true } + +describe('LoginRequest', () => { + describe('loginPassword()', () => { + let res, req + + beforeEach(() => { + req = { + app: { locals: { oidc: { users: mockUserStore }, localAuth, accountManager } }, + body: { username: 'alice', password: '12345' } + } + res = HttpMocks.createResponse() + }) + + it('should create a LoginRequest instance', () => { + let fromParams = sinon.spy(LoginRequest, 'fromParams') + let loginStub = sinon.stub(LoginRequest, 'login') + .returns(Promise.resolve()) + + return LoginRequest.loginPassword(req, res) + .then(() => { + expect(fromParams).to.have.been.calledWith(req, res) + fromParams.reset() + loginStub.restore() + }) + }) + + it('should invoke login()', () => { + let login = sinon.spy(LoginRequest, 'login') + + return LoginRequest.loginPassword(req, res) + .then(() => { + expect(login).to.have.been.called() + login.reset() + }) + }) + }) + + describe('loginTls()', () => { + let res, req + + beforeEach(() => { + req = { + connection: {}, + app: { locals: { localAuth, accountManager } } + } + res = HttpMocks.createResponse() + }) + + it('should create a LoginRequest instance', () => { + return LoginRequest.loginTls(req, res) + .then(() => { + expect(LoginRequest.fromParams).to.have.been.calledWith(req, res) + LoginRequest.fromParams.reset() + LoginRequest.login.reset() + }) + }) + + it('should invoke login()', () => { + return LoginRequest.loginTls(req, res) + .then(() => { + expect(LoginRequest.login).to.have.been.called() + LoginRequest.login.reset() + }) + }) + }) + + describe('fromParams()', () => { + let session = {} + let req = { + session, + app: { locals: { accountManager } }, + body: { username: 'alice', password: '12345' } + } + let res = HttpMocks.createResponse() + + it('should return a LoginRequest instance', () => { + let request = LoginRequest.fromParams(req, res) + + expect(request.response).to.equal(res) + expect(request.session).to.equal(session) + expect(request.accountManager).to.equal(accountManager) + }) + + it('should initialize the query params', () => { + let requestOptions = sinon.spy(AuthRequest, 'requestOptions') + LoginRequest.fromParams(req, res) + + expect(requestOptions).to.have.been.calledWith(req) + }) + }) + + describe('login()', () => { + let userStore = mockUserStore + let response + + let options = { + userStore, + accountManager, + localAuth: {} + } + + beforeEach(() => { + response = HttpMocks.createResponse() + }) + + it('should call initUserSession() for a valid user', () => { + let validUser = {} + options.response = response + options.authenticator = { + findValidUser: sinon.stub().resolves(validUser) + } + + let request = new LoginRequest(options) + + let initUserSession = sinon.spy(request, 'initUserSession') + + return LoginRequest.login(request) + .then(() => { + expect(initUserSession).to.have.been.calledWith(validUser) + }) + }) + + it('should call redirectPostLogin()', () => { + let validUser = {} + options.response = response + options.authenticator = { + findValidUser: sinon.stub().resolves(validUser) + } + + let request = new LoginRequest(options) + + let redirectPostLogin = sinon.spy(request, 'redirectPostLogin') + + return LoginRequest.login(request) + .then(() => { + expect(redirectPostLogin).to.have.been.calledWith(validUser) + }) + }) + }) + + describe('postLoginUrl()', () => { + it('should return the user account uri if no redirect_uri param', () => { + let request = new LoginRequest({ authQueryParams: {} }) + + let aliceAccount = 'https://alice.example.com' + let user = { accountUri: aliceAccount } + + expect(request.postLoginUrl(user)).to.equal(aliceAccount) + }) + }) + + describe('redirectPostLogin()', () => { + it('should redirect to the /authorize url if redirect_uri is present', () => { + let res = HttpMocks.createResponse() + let authUrl = 'https://localhost:8443/authorize?redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback' + let validUser = accountManager.userAccountFrom({ username: 'alice' }) + + let authQueryParams = { + redirect_uri: 'https://app.example.com/callback' + } + + let options = { accountManager, authQueryParams, response: res } + let request = new LoginRequest(options) + + request.authorizeUrl = sinon.stub().returns(authUrl) + + request.redirectPostLogin(validUser) + + expect(res.statusCode).to.equal(302) + expect(res._getRedirectUrl()).to.equal(authUrl) + }) + + it('should redirect to account uri if no redirect_uri present', () => { + let res = HttpMocks.createResponse() + let authUrl = 'https://localhost/authorize?client_id=123' + let validUser = accountManager.userAccountFrom({ username: 'alice' }) + + let authQueryParams = {} + + let options = { accountManager, authQueryParams, response: res } + let request = new LoginRequest(options) + + request.authorizeUrl = sinon.stub().returns(authUrl) + + request.redirectPostLogin(validUser) + + let expectedUri = accountManager.accountUriFor('alice') + expect(res.statusCode).to.equal(302) + expect(res._getRedirectUrl()).to.equal(expectedUri) + }) + + it('should redirect to account uri if redirect_uri is string "undefined', () => { + let res = HttpMocks.createResponse() + let authUrl = 'https://localhost/authorize?client_id=123' + let validUser = accountManager.userAccountFrom({ username: 'alice' }) + + let body = { redirect_uri: 'undefined' } + + let options = { accountManager, response: res } + let request = new LoginRequest(options) + request.authQueryParams = AuthRequest.extractAuthParams({ body }) + + request.authorizeUrl = sinon.stub().returns(authUrl) + + request.redirectPostLogin(validUser) + + let expectedUri = accountManager.accountUriFor('alice') + + expect(res.statusCode).to.equal(302) + expect(res._getRedirectUrl()).to.equal(expectedUri) + }) + }) +}) diff --git a/test/unit/oidc-manager.js b/test/unit/oidc-manager.js new file mode 100644 index 000000000..4c49da090 --- /dev/null +++ b/test/unit/oidc-manager.js @@ -0,0 +1,37 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const path = require('path') + +const OidcManager = require('../../lib/models/oidc-manager') +const SolidHost = require('../../lib/models/solid-host') + +describe('OidcManager', () => { + describe('fromServerConfig()', () => { + it('should error if no serverUri is provided in argv', () => { + + }) + + it('should result in an initialized oidc object', () => { + let serverUri = 'https://localhost:8443' + let host = SolidHost.from({ serverUri }) + + let dbPath = path.join(__dirname, '../resources/db') + let saltRounds = 5 + let argv = { + host, + dbPath, + saltRounds + } + + let oidc = OidcManager.fromServerConfig(argv) + + expect(oidc.rs.defaults.query).to.be.true + expect(oidc.clients.store.backend.path.endsWith('db/rp/clients')) + expect(oidc.provider.issuer).to.equal(serverUri) + expect(oidc.users.backend.path.endsWith('db/users')) + expect(oidc.users.saltRounds).to.equal(saltRounds) + }) + }) +}) diff --git a/test/unit/password-authenticator.js b/test/unit/password-authenticator.js new file mode 100644 index 000000000..c93c094f0 --- /dev/null +++ b/test/unit/password-authenticator.js @@ -0,0 +1,228 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.should() + +const { PasswordAuthenticator } = require('../../lib/models/authenticator') + +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') + +const mockUserStore = { + findUser: () => { return Promise.resolve(true) }, + matchPassword: (user, password) => { return Promise.resolve(user) } +} + +const host = SolidHost.from({ serverUri: 'https://localhost:8443' }) +const accountManager = AccountManager.from({ host }) + +describe('PasswordAuthenticator', () => { + describe('fromParams()', () => { + let req = { + body: { username: 'alice', password: '12345' } + } + let options = { userStore: mockUserStore, accountManager } + + it('should return a PasswordAuthenticator instance', () => { + let pwAuth = PasswordAuthenticator.fromParams(req, options) + + expect(pwAuth.userStore).to.equal(mockUserStore) + expect(pwAuth.accountManager).to.equal(accountManager) + expect(pwAuth.username).to.equal('alice') + expect(pwAuth.password).to.equal('12345') + }) + + it('should init with undefined username and password if no body is provided', () => { + let req = {} + + let pwAuth = PasswordAuthenticator.fromParams(req, {}) + + expect(pwAuth.username).to.be.undefined() + expect(pwAuth.password).to.be.undefined() + }) + }) + + describe('validate()', () => { + it('should throw a 400 error if no username was provided', done => { + let options = { username: null, password: '12345' } + let pwAuth = new PasswordAuthenticator(options) + + try { + pwAuth.validate() + } catch (error) { + expect(error.statusCode).to.equal(400) + expect(error.message).to.equal('Username required') + done() + } + }) + + it('should throw a 400 error if no password was provided', done => { + let options = { username: 'alice', password: null } + let pwAuth = new PasswordAuthenticator(options) + + try { + pwAuth.validate() + } catch (error) { + expect(error.statusCode).to.equal(400) + expect(error.message).to.equal('Password required') + done() + } + }) + }) + + describe('findValidUser()', () => { + it('should throw a 400 if no valid user is found in the user store', done => { + let options = { + username: 'alice', + password: '1234', + accountManager + } + let pwAuth = new PasswordAuthenticator(options) + + pwAuth.userStore = { + findUser: () => { return Promise.resolve(false) } + } + + pwAuth.findValidUser() + .catch(error => { + expect(error.statusCode).to.equal(400) + expect(error.message).to.equal('No user found for that username') + done() + }) + }) + + it('should throw a 400 if user is found but password does not match', done => { + let options = { + username: 'alice', + password: '1234', + accountManager + } + let pwAuth = new PasswordAuthenticator(options) + + pwAuth.userStore = { + findUser: () => { return Promise.resolve(true) }, + matchPassword: () => { return Promise.resolve(false) } + } + + pwAuth.findValidUser() + .catch(error => { + expect(error.statusCode).to.equal(400) + expect(error.message).to.equal('User found but no password match') + done() + }) + }) + + it('should return a valid user if one is found and password matches', () => { + let webId = 'https://alice.example.com/#me' + let validUser = { username: 'alice', webId } + let options = { + username: 'alice', + password: '1234', + accountManager + } + let pwAuth = new PasswordAuthenticator(options) + + pwAuth.userStore = { + findUser: () => { return Promise.resolve(validUser) }, + matchPassword: (user, password) => { return Promise.resolve(user) } + } + + return pwAuth.findValidUser() + .then(foundUser => { + expect(foundUser.webId).to.equal(webId) + }) + }) + + describe('in Multi User mode', () => { + let multiUser = true + let serverUri = 'https://example.com' + let host = SolidHost.from({ serverUri }) + + let accountManager = AccountManager.from({ multiUser, host }) + + let aliceRecord = { webId: 'https://alice.example.com/profile/card#me' } + let mockUserStore = { + findUser: sinon.stub().resolves(aliceRecord), + matchPassword: (user, password) => { return Promise.resolve(user) } + } + + it('should load user from store if provided with username', () => { + let options = { + username: 'alice', + password: '1234', + userStore: mockUserStore, + accountManager + } + let pwAuth = new PasswordAuthenticator(options) + + let userStoreKey = 'alice.example.com/profile/card#me' + + return pwAuth.findValidUser() + .then(() => { + expect(mockUserStore.findUser).to.be.calledWith(userStoreKey) + }) + }) + + it('should load user from store if provided with WebID', () => { + let webId = 'https://alice.example.com/profile/card#me' + let options = { + username: webId, + password: '1234', + userStore: mockUserStore, + accountManager + } + let pwAuth = new PasswordAuthenticator(options) + + let userStoreKey = 'alice.example.com/profile/card#me' + + return pwAuth.findValidUser() + .then(() => { + expect(mockUserStore.findUser).to.be.calledWith(userStoreKey) + }) + }) + }) + + describe('in Single User mode', () => { + let multiUser = false + let serverUri = 'https://localhost:8443' + let host = SolidHost.from({ serverUri }) + + let accountManager = AccountManager.from({ multiUser, host }) + + let aliceRecord = { webId: 'https://localhost:8443/profile/card#me' } + let mockUserStore = { + findUser: sinon.stub().resolves(aliceRecord), + matchPassword: (user, password) => { return Promise.resolve(user) } + } + + it('should load user from store if provided with username', () => { + let options = { username: 'admin', password: '1234', userStore: mockUserStore, accountManager } + let pwAuth = new PasswordAuthenticator(options) + + let userStoreKey = 'localhost:8443/profile/card#me' + + return pwAuth.findValidUser() + .then(() => { + expect(mockUserStore.findUser).to.be.calledWith(userStoreKey) + }) + }) + + it('should load user from store if provided with WebID', () => { + let webId = 'https://localhost:8443/profile/card#me' + let options = { username: webId, password: '1234', userStore: mockUserStore, accountManager } + let pwAuth = new PasswordAuthenticator(options) + + let userStoreKey = 'localhost:8443/profile/card#me' + + return pwAuth.findValidUser() + .then(() => { + expect(mockUserStore.findUser).to.be.calledWith(userStoreKey) + }) + }) + }) + }) +}) diff --git a/test/unit/password-change-request.js b/test/unit/password-change-request.js new file mode 100644 index 000000000..50943e273 --- /dev/null +++ b/test/unit/password-change-request.js @@ -0,0 +1,260 @@ +'use strict' + +const chai = require('chai') +const sinon = require('sinon') +const expect = chai.expect +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const sinonChai = require('sinon-chai') +chai.use(sinonChai) +chai.should() + +const HttpMocks = require('node-mocks-http') + +const PasswordChangeRequest = require('../../lib/requests/password-change-request') +const SolidHost = require('../../lib/models/solid-host') + +describe('PasswordChangeRequest', () => { + sinon.spy(PasswordChangeRequest.prototype, 'error') + + describe('constructor()', () => { + it('should initialize a request instance from options', () => { + let res = HttpMocks.createResponse() + + let accountManager = {} + let userStore = {} + + let options = { + accountManager, + userStore, + returnToUrl: 'https://example.com/resource', + response: res, + token: '12345', + newPassword: 'swordfish' + } + + let request = new PasswordChangeRequest(options) + + expect(request.returnToUrl).to.equal(options.returnToUrl) + expect(request.response).to.equal(res) + expect(request.token).to.equal(options.token) + expect(request.newPassword).to.equal(options.newPassword) + expect(request.accountManager).to.equal(accountManager) + expect(request.userStore).to.equal(userStore) + }) + }) + + describe('fromParams()', () => { + it('should return a request instance from options', () => { + let returnToUrl = 'https://example.com/resource' + let token = '12345' + let newPassword = 'swordfish' + let accountManager = {} + let userStore = {} + + let req = { + app: { locals: { accountManager, oidc: { users: userStore } } }, + query: { returnToUrl, token }, + body: { newPassword } + } + let res = HttpMocks.createResponse() + + let request = PasswordChangeRequest.fromParams(req, res) + + expect(request.returnToUrl).to.equal(returnToUrl) + expect(request.response).to.equal(res) + expect(request.token).to.equal(token) + expect(request.newPassword).to.equal(newPassword) + expect(request.accountManager).to.equal(accountManager) + expect(request.userStore).to.equal(userStore) + }) + }) + + describe('get()', () => { + let returnToUrl = 'https://example.com/resource' + let token = '12345' + let userStore = {} + let res = HttpMocks.createResponse() + sinon.spy(res, 'render') + + it('should create an instance and render a change password form', () => { + let accountManager = { + validateResetToken: sinon.stub().resolves(true) + } + let req = { + app: { locals: { accountManager, oidc: { users: userStore } } }, + query: { returnToUrl, token } + } + + return PasswordChangeRequest.get(req, res) + .then(() => { + expect(accountManager.validateResetToken) + .to.have.been.called() + expect(res.render).to.have.been.calledWith('auth/change-password', + { returnToUrl, token, validToken: true }) + }) + }) + + it('should display an error message on an invalid token', () => { + let accountManager = { + validateResetToken: sinon.stub().throws() + } + let req = { + app: { locals: { accountManager, oidc: { users: userStore } } }, + query: { returnToUrl, token } + } + + return PasswordChangeRequest.get(req, res) + .then(() => { + expect(PasswordChangeRequest.prototype.error) + .to.have.been.called() + }) + }) + }) + + describe('post()', () => { + it('creates a request instance and invokes handlePost()', () => { + sinon.spy(PasswordChangeRequest, 'handlePost') + + let returnToUrl = 'https://example.com/resource' + let token = '12345' + let newPassword = 'swordfish' + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let alice = { + webId: 'https://alice.example.com/#me' + } + let storedToken = { webId: alice.webId } + let store = { + findUser: sinon.stub().resolves(alice), + updatePassword: sinon.stub() + } + let accountManager = { + host, + store, + userAccountFrom: sinon.stub().resolves(alice), + validateResetToken: sinon.stub().resolves(storedToken) + } + + accountManager.accountExists = sinon.stub().resolves(true) + accountManager.loadAccountRecoveryEmail = sinon.stub().resolves('alice@example.com') + accountManager.sendPasswordResetEmail = sinon.stub().resolves() + + let req = { + app: { locals: { accountManager, oidc: { users: store } } }, + query: { returnToUrl }, + body: { token, newPassword } + } + let res = HttpMocks.createResponse() + + return PasswordChangeRequest.post(req, res) + .then(() => { + expect(PasswordChangeRequest.handlePost).to.have.been.called() + }) + }) + }) + + describe('handlePost()', () => { + it('should display error message if validation error encountered', () => { + let returnToUrl = 'https://example.com/resource' + let token = '12345' + let userStore = {} + let res = HttpMocks.createResponse() + let accountManager = { + validateResetToken: sinon.stub().throws() + } + let req = { + app: { locals: { accountManager, oidc: { users: userStore } } }, + query: { returnToUrl, token } + } + + let request = PasswordChangeRequest.fromParams(req, res) + + return PasswordChangeRequest.handlePost(request) + .then(() => { + expect(PasswordChangeRequest.prototype.error) + .to.have.been.called() + }) + }) + }) + + describe('validateToken()', () => { + it('should return false if no token is present', () => { + let accountManager = { + validateResetToken: sinon.stub() + } + let request = new PasswordChangeRequest({ accountManager, token: null }) + + return request.validateToken() + .then(result => { + expect(result).to.be.false() + expect(accountManager.validateResetToken).to.not.have.been.called() + }) + }) + }) + + describe('validatePost()', () => { + it('should throw an error if no new password was entered', () => { + let request = new PasswordChangeRequest({ newPassword: null }) + + expect(() => request.validatePost()).to.throw('Please enter a new password') + }) + }) + + describe('error()', () => { + it('should invoke renderForm() with the error', () => { + let request = new PasswordChangeRequest({}) + request.renderForm = sinon.stub() + let error = new Error('error message') + + request.error(error) + + expect(request.renderForm).to.have.been.calledWith(error) + }) + }) + + describe('changePassword()', () => { + it('should create a new user store entry if none exists', () => { + // this would be the case for legacy pre-user-store accounts + let webId = 'https://alice.example.com/#me' + let user = { webId, id: webId } + let accountManager = { + userAccountFrom: sinon.stub().returns(user) + } + let userStore = { + findUser: sinon.stub().resolves(null), // no user found + createUser: sinon.stub().resolves(), + updatePassword: sinon.stub().resolves() + } + + let options = { + accountManager, userStore, newPassword: 'swordfish' + } + let request = new PasswordChangeRequest(options) + + return request.changePassword(user) + .then(() => { + expect(userStore.createUser).to.have.been.calledWith(user, options.newPassword) + }) + }) + }) + + describe('renderForm()', () => { + it('should set response status to error status, if error exists', () => { + let returnToUrl = 'https://example.com/resource' + let token = '12345' + let response = HttpMocks.createResponse() + sinon.spy(response, 'render') + + let options = { returnToUrl, token, response } + + let request = new PasswordChangeRequest(options) + + let error = new Error('error message') + + request.renderForm(error) + + expect(response.render).to.have.been.calledWith('auth/change-password', + { validToken: false, token, returnToUrl, error: 'error message' }) + }) + }) +}) diff --git a/test/unit/password-reset-email-request.js b/test/unit/password-reset-email-request.js new file mode 100644 index 000000000..74fed9035 --- /dev/null +++ b/test/unit/password-reset-email-request.js @@ -0,0 +1,192 @@ +'use strict' + +const chai = require('chai') +const sinon = require('sinon') +const expect = chai.expect +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +const sinonChai = require('sinon-chai') +chai.use(sinonChai) +chai.should() + +const HttpMocks = require('node-mocks-http') + +const PasswordResetEmailRequest = require('../../lib/requests/password-reset-email-request') +const AccountManager = require('../../lib/models/account-manager') +const SolidHost = require('../../lib/models/solid-host') + +describe('PasswordResetEmailRequest', () => { + describe('constructor()', () => { + it('should initialize a request instance from options', () => { + let res = HttpMocks.createResponse() + + let options = { + returnToUrl: 'https://example.com/resource', + response: res, + username: 'alice' + } + + let request = new PasswordResetEmailRequest(options) + + expect(request.returnToUrl).to.equal(options.returnToUrl) + expect(request.response).to.equal(res) + expect(request.username).to.equal(options.username) + }) + }) + + describe('fromParams()', () => { + it('should return a request instance from options', () => { + let returnToUrl = 'https://example.com/resource' + let username = 'alice' + let accountManager = {} + + let req = { + app: { locals: { accountManager } }, + query: { returnToUrl }, + body: { username } + } + let res = HttpMocks.createResponse() + + let request = PasswordResetEmailRequest.fromParams(req, res) + + expect(request.accountManager).to.equal(accountManager) + expect(request.returnToUrl).to.equal(returnToUrl) + expect(request.username).to.equal(username) + expect(request.response).to.equal(res) + }) + }) + + describe('get()', () => { + it('should create an instance and render a reset password form', () => { + let returnToUrl = 'https://example.com/resource' + let username = 'alice' + let accountManager = { multiUser: true } + + let req = { + app: { locals: { accountManager } }, + query: { returnToUrl }, + body: { username } + } + let res = HttpMocks.createResponse() + res.render = sinon.stub() + + PasswordResetEmailRequest.get(req, res) + + expect(res.render).to.have.been.calledWith('auth/reset-password', + { returnToUrl, multiUser: true }) + }) + }) + + describe('post()', () => { + it('creates a request instance and invokes handlePost()', () => { + sinon.spy(PasswordResetEmailRequest, 'handlePost') + + let returnToUrl = 'https://example.com/resource' + let username = 'alice' + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let store = { + suffixAcl: '.acl' + } + let accountManager = AccountManager.from({ host, multiUser: true, store }) + accountManager.accountExists = sinon.stub().resolves(true) + accountManager.loadAccountRecoveryEmail = sinon.stub().resolves('alice@example.com') + accountManager.sendPasswordResetEmail = sinon.stub().resolves() + + let req = { + app: { locals: { accountManager } }, + query: { returnToUrl }, + body: { username } + } + let res = HttpMocks.createResponse() + + PasswordResetEmailRequest.post(req, res) + .then(() => { + expect(PasswordResetEmailRequest.handlePost).to.have.been.called() + }) + }) + }) + + describe('validate()', () => { + it('should throw an error if username is missing in multiUser mode', () => { + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let accountManager = AccountManager.from({ host, multiUser: true }) + + let request = new PasswordResetEmailRequest({ accountManager }) + + expect(() => request.validate()).to.throw(/Username required/) + }) + + it('should not throw an error if username is missing in single user mode', () => { + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let accountManager = AccountManager.from({ host, multiUser: false }) + + let request = new PasswordResetEmailRequest({ accountManager }) + + expect(() => request.validate()).to.not.throw() + }) + }) + + describe('handlePost()', () => { + it('should handle the post request', () => { + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let store = { suffixAcl: '.acl' } + let accountManager = AccountManager.from({ host, multiUser: true, store }) + accountManager.loadAccountRecoveryEmail = sinon.stub().resolves('alice@example.com') + accountManager.sendPasswordResetEmail = sinon.stub().resolves() + accountManager.accountExists = sinon.stub().resolves(true) + + let returnToUrl = 'https://example.com/resource' + let username = 'alice' + let response = HttpMocks.createResponse() + response.render = sinon.stub() + + let options = { accountManager, username, returnToUrl, response } + let request = new PasswordResetEmailRequest(options) + + sinon.spy(request, 'error') + + return PasswordResetEmailRequest.handlePost(request) + .then(() => { + expect(accountManager.loadAccountRecoveryEmail).to.have.been.called() + expect(accountManager.sendPasswordResetEmail).to.have.been.called() + expect(response.render).to.have.been.calledWith('auth/reset-link-sent') + expect(request.error).to.not.have.been.called() + }) + }) + }) + + describe('loadUser()', () => { + it('should return a UserAccount instance based on username', () => { + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let store = { suffixAcl: '.acl' } + let accountManager = AccountManager.from({ host, multiUser: true, store }) + accountManager.accountExists = sinon.stub().resolves(true) + let username = 'alice' + + let options = { accountManager, username } + let request = new PasswordResetEmailRequest(options) + + return request.loadUser() + .then(account => { + expect(account.webId).to.equal('https://alice.example.com/profile/card#me') + }) + }) + + it('should throw an error if the user does not exist', done => { + let host = SolidHost.from({ serverUri: 'https://example.com' }) + let store = { suffixAcl: '.acl' } + let accountManager = AccountManager.from({ host, multiUser: true, store }) + accountManager.accountExists = sinon.stub().resolves(false) + let username = 'alice' + + let options = { accountManager, username } + let request = new PasswordResetEmailRequest(options) + + request.loadUser() + .catch(error => { + expect(error.message).to.equal('Account not found for that username') + done() + }) + }) + }) +}) diff --git a/test/solid-host.js b/test/unit/solid-host.js similarity index 73% rename from test/solid-host.js rename to test/unit/solid-host.js index 510bc43ee..078d46b1e 100644 --- a/test/solid-host.js +++ b/test/unit/solid-host.js @@ -2,8 +2,8 @@ const expect = require('chai').expect -const SolidHost = require('../lib/models/solid-host') -const defaults = require('../config/defaults') +const SolidHost = require('../../lib/models/solid-host') +const defaults = require('../../config/defaults') describe('SolidHost', () => { describe('from()', () => { @@ -21,8 +21,8 @@ describe('SolidHost', () => { it('should init to default port and serverUri values', () => { let host = SolidHost.from({}) - expect(host.port).to.equal(defaults.DEFAULT_PORT) - expect(host.serverUri).to.equal(defaults.DEFAULT_URI) + expect(host.port).to.equal(defaults.port) + expect(host.serverUri).to.equal(defaults.serverUri) }) }) @@ -59,4 +59,17 @@ describe('SolidHost', () => { expect(host.cookieDomain).to.equal('.example.com') }) }) + + describe('authEndpoint getter', () => { + it('should return an /authorize url object', () => { + let host = SolidHost.from({ + serverUri: 'https://localhost:8443' + }) + + let authUrl = host.authEndpoint + + expect(authUrl.host).to.equal('localhost:8443') + expect(authUrl.path).to.equal('/authorize') + }) + }) }) diff --git a/test/unit/tls-authenticator.js b/test/unit/tls-authenticator.js new file mode 100644 index 000000000..0ae6a3d2e --- /dev/null +++ b/test/unit/tls-authenticator.js @@ -0,0 +1,169 @@ +'use strict' + +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +chai.use(require('sinon-chai')) +chai.use(require('dirty-chai')) +chai.use(require('chai-as-promised')) +chai.should() + +const { TlsAuthenticator } = require('../../lib/models/authenticator') + +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') + +const host = SolidHost.from({ serverUri: 'https://example.com' }) +const accountManager = AccountManager.from({ host, multiUser: true }) + +describe('TlsAuthenticator', () => { + describe('fromParams()', () => { + let req = { + connection: {} + } + let options = { accountManager } + + it('should return a TlsAuthenticator instance', () => { + let tlsAuth = TlsAuthenticator.fromParams(req, options) + + expect(tlsAuth.accountManager).to.equal(accountManager) + expect(tlsAuth.connection).to.equal(req.connection) + }) + }) + + describe('findValidUser()', () => { + let webId = 'https://alice.example.com/#me' + let certificate = { uri: webId } + let connection = { + renegotiate: sinon.stub().yields(), + getPeerCertificate: sinon.stub().returns(certificate) + } + let options = { accountManager, connection } + + let tlsAuth = new TlsAuthenticator(options) + + tlsAuth.extractWebId = sinon.stub().resolves(webId) + sinon.spy(tlsAuth, 'renegotiateTls') + sinon.spy(tlsAuth, 'ensureLocalUser') + + return tlsAuth.findValidUser() + .then(validUser => { + expect(tlsAuth.renegotiateTls).to.have.been.called() + expect(connection.getPeerCertificate).to.have.been.called() + expect(tlsAuth.extractWebId).to.have.been.calledWith(certificate) + expect(tlsAuth.ensureLocalUser).to.have.been.calledWith(webId) + + expect(validUser.webId).to.equal(webId) + }) + }) + + describe('renegotiateTls()', () => { + it('should reject if an error occurs while renegotiating', () => { + let connection = { + renegotiate: sinon.stub().yields(new Error('Error renegotiating')) + } + + let tlsAuth = new TlsAuthenticator({ connection }) + + expect(tlsAuth.renegotiateTls()).to.be.rejectedWith(/Error renegotiating/) + }) + + it('should resolve if no error occurs', () => { + let connection = { + renegotiate: sinon.stub().yields(null) + } + + let tlsAuth = new TlsAuthenticator({ connection }) + + expect(tlsAuth.renegotiateTls()).to.be.fulfilled() + }) + }) + + describe('getCertificate()', () => { + it('should throw on a non-existent certificate', () => { + let connection = { + getPeerCertificate: sinon.stub().returns(null) + } + + let tlsAuth = new TlsAuthenticator({ connection }) + + expect(() => tlsAuth.getCertificate()).to.throw(/No client certificate detected/) + }) + + it('should throw on an empty certificate', () => { + let connection = { + getPeerCertificate: sinon.stub().returns({}) + } + + let tlsAuth = new TlsAuthenticator({ connection }) + + expect(() => tlsAuth.getCertificate()).to.throw(/No client certificate detected/) + }) + + it('should return a certificate if no error occurs', () => { + let certificate = { uri: 'https://alice.example.com/#me' } + let connection = { + getPeerCertificate: sinon.stub().returns(certificate) + } + + let tlsAuth = new TlsAuthenticator({ connection }) + + expect(tlsAuth.getCertificate()).to.equal(certificate) + }) + }) + + describe('extractWebId()', () => { + it('should reject if an error occurs verifying certificate', () => { + let tlsAuth = new TlsAuthenticator({}) + + tlsAuth.verifyWebId = sinon.stub().yields(new Error('Error processing certificate')) + + expect(tlsAuth.extractWebId()).to.be.rejectedWith(/Error processing certificate/) + }) + + it('should resolve with a verified web id', () => { + let tlsAuth = new TlsAuthenticator({}) + + let webId = 'https://alice.example.com/#me' + tlsAuth.verifyWebId = sinon.stub().yields(null, webId) + + let certificate = { uri: webId } + + expect(tlsAuth.extractWebId(certificate)).to.become(webId) + }) + }) + + describe('ensureLocalUser()', () => { + it('should throw an error if the user is not local to this server', () => { + let tlsAuth = new TlsAuthenticator({ accountManager }) + + let externalWebId = 'https://alice.someothersite.com#me' + + expect(() => tlsAuth.ensureLocalUser(externalWebId)) + .to.throw(/Cannot login: Selected Web ID is not hosted on this server/) + }) + + it('should return a user instance if the webid is local', () => { + let tlsAuth = new TlsAuthenticator({ accountManager }) + + let webId = 'https://alice.example.com/#me' + + let user = tlsAuth.ensureLocalUser(webId) + + expect(user.username).to.equal('alice') + expect(user.webId).to.equal(webId) + }) + }) + + describe('verifyWebId()', () => { + it('should yield an error if no cert is given', done => { + let tlsAuth = new TlsAuthenticator({}) + + tlsAuth.verifyWebId(null, (error) => { + expect(error.message).to.equal('No certificate given') + + done() + }) + }) + }) +}) diff --git a/test/unit/token-service.js b/test/unit/token-service.js new file mode 100644 index 000000000..aa528dd41 --- /dev/null +++ b/test/unit/token-service.js @@ -0,0 +1,72 @@ +'use strict' + +const moment = require('moment') +const chai = require('chai') +const expect = chai.expect +const dirtyChai = require('dirty-chai') +chai.use(dirtyChai) +chai.should() + +const TokenService = require('../../lib/models/token-service') + +describe('TokenService', () => { + describe('constructor()', () => { + it('should init with an empty tokens store', () => { + let service = new TokenService() + + expect(service.tokens).to.exist() + }) + }) + + describe('generate()', () => { + it('should generate a new token and return a token key', () => { + let service = new TokenService() + + let token = service.generate() + let value = service.tokens[token] + + expect(token).to.exist() + expect(value).to.have.property('exp') + }) + }) + + describe('verify()', () => { + it('should return false for expired tokens', () => { + let service = new TokenService() + + let token = service.generate() + + service.tokens[token].exp = moment().subtract(40, 'minutes') + + expect(service.verify(token)).to.be.false() + }) + + it('should return false for non-existent tokens', () => { + let service = new TokenService() + + let token = 'invalid token 123' + + expect(service.verify(token)).to.be.false() + }) + + it('should return the token value if token not expired', () => { + let service = new TokenService() + + let token = service.generate() + + expect(service.verify(token)).to.be.ok() + }) + }) + + describe('remove()', () => { + it('should remove a generated token from the service', () => { + let service = new TokenService() + + let token = service.generate() + + service.remove(token) + + expect(service.tokens[token]).to.not.exist() + }) + }) +}) diff --git a/test/user-account.js b/test/unit/user-account.js similarity index 55% rename from test/user-account.js rename to test/unit/user-account.js index b84002e8f..420a346a7 100644 --- a/test/user-account.js +++ b/test/unit/user-account.js @@ -2,7 +2,7 @@ const chai = require('chai') const expect = chai.expect -const UserAccount = require('../lib/models/user-account') +const UserAccount = require('../../lib/models/user-account') describe('UserAccount', () => { describe('from()', () => { @@ -21,4 +21,19 @@ describe('UserAccount', () => { expect(account.email).to.equal(options.email) }) }) + + describe('id getter', () => { + it('should return null if webId is null', () => { + let account = new UserAccount() + + expect(account.id).to.be.null + }) + + it('should return the WebID uri minus the protocol and slashes', () => { + let webId = 'https://alice.example.com/profile/card#me' + let account = new UserAccount({ webId }) + + expect(account.id).to.equal('alice.example.com/profile/card#me') + }) + }) }) diff --git a/test/unit/user-accounts-api.js b/test/unit/user-accounts-api.js new file mode 100644 index 000000000..4436064fd --- /dev/null +++ b/test/unit/user-accounts-api.js @@ -0,0 +1,54 @@ +'use strict' + +const path = require('path') +const chai = require('chai') +const expect = chai.expect +// const sinon = require('sinon') +// const sinonChai = require('sinon-chai') +// chai.use(sinonChai) +chai.should() +const HttpMocks = require('node-mocks-http') + +const LDP = require('../../lib/ldp') +const SolidHost = require('../../lib/models/solid-host') +const AccountManager = require('../../lib/models/account-manager') +const testAccountsDir = path.join(__dirname, '..', 'resources', 'accounts') + +const api = require('../../lib/api/accounts/user-accounts') + +var host + +beforeEach(() => { + host = SolidHost.from({ serverUri: 'https://example.com' }) +}) + +describe('api/accounts/user-accounts', () => { + describe('newCertificate()', () => { + describe('in multi user mode', () => { + let multiUser = true + let store = new LDP({ root: testAccountsDir, idp: multiUser }) + + it('should throw a 400 error if spkac param is missing', done => { + let options = { host, store, multiUser, authMethod: 'oidc' } + let accountManager = AccountManager.from(options) + + let req = { + body: { + webid: 'https://alice.example.com/#me' + }, + session: { userId: 'https://alice.example.com/#me' }, + get: () => { return 'https://example.com' } + } + let res = HttpMocks.createResponse() + + let newCertificate = api.newCertificate(accountManager) + + newCertificate(req, res, (err) => { + expect(err.status).to.equal(400) + expect(err.message).to.equal('Missing spkac parameter') + done() + }) + }) + }) + }) +}) diff --git a/test/utils.js b/test/unit/utils.js similarity index 63% rename from test/utils.js rename to test/unit/utils.js index af2d3c245..7d5d3fc96 100644 --- a/test/utils.js +++ b/test/unit/utils.js @@ -1,6 +1,6 @@ var assert = require('chai').assert -var utils = require('../lib/utils') +var utils = require('../../lib/utils') describe('Utility functions', function () { describe('pathBasename', function () { @@ -49,4 +49,37 @@ describe('Utility functions', function () { assert.equal(utils.stripLineEndings(str), '123456') }) }) + + describe('debrack()', () => { + it('should return null if no string is passed', () => { + assert.equal(utils.debrack(), null) + }) + + it('should return the string if no brackets are present', () => { + assert.equal(utils.debrack('test string'), 'test string') + }) + + it('should return the string if less than 2 chars long', () => { + assert.equal(utils.debrack(''), '') + assert.equal(utils.debrack('<'), '<') + }) + + it('should remove brackets if wrapping the string', () => { + assert.equal(utils.debrack(''), 'test string') + }) + }) + + describe('fullUrlForReq()', () => { + it('should extract a fully-qualified url from an Express request', () => { + let req = { + protocol: 'https:', + get: (host) => 'example.com', + baseUrl: '/', + path: '/resource1', + query: { sort: 'desc' } + } + + assert.equal(utils.fullUrlForReq(req), 'https://example.com/resource1?sort=desc') + }) + }) }) diff --git a/test/user-accounts-api.js b/test/user-accounts-api.js deleted file mode 100644 index fe8822304..000000000 --- a/test/user-accounts-api.js +++ /dev/null @@ -1,97 +0,0 @@ -'use strict' - -const path = require('path') -const chai = require('chai') -const expect = chai.expect -const sinon = require('sinon') -const sinonChai = require('sinon-chai') -chai.use(sinonChai) -chai.should() -const HttpMocks = require('node-mocks-http') - -const LDP = require('../lib/ldp') -const SolidHost = require('../lib/models/solid-host') -const AccountManager = require('../lib/models/account-manager') -const { CreateAccountRequest } = require('../lib/requests/create-account-request') -const testAccountsDir = path.join(__dirname, 'resources', 'accounts') - -const api = require('../lib/api/accounts/user-accounts') - -var host - -beforeEach(() => { - host = SolidHost.from({ serverUri: 'https://example.com' }) -}) - -describe('api/accounts/user-accounts', () => { - describe('createAccount()', () => { - let multiUser = true - let store = new LDP({ root: testAccountsDir, idp: multiUser }) - - it('should call create and invoke a CreateAccountRequest instance', () => { - let options = { host, store, multiUser, authMethod: 'tls' } - let accountManager = AccountManager.from(options) - - let createAccountSpy = sinon.spy(CreateAccountRequest.prototype, 'createAccount') - let req = { - body: { username: 'alice', spkac: '123' }, - session: {} - } - let res = HttpMocks.createResponse() - - let createAccount = api.createAccount(accountManager) - - createAccount(req, res, (err) => { - expect(err).to.not.exist - expect(createAccountSpy).to.have.been.called - createAccountSpy.restore() - }) - }) - - it('should call next(error) if an exception occurs in fromParams()', done => { - let options = { host, store, multiUser, authMethod: 'tls' } - let accountManager = AccountManager.from(options) - let req = { - body: { username: null }, - session: {} - } - let res = HttpMocks.createResponse() - - let createAccount = api.createAccount(accountManager) - - createAccount(req, res, (err) => { - expect(err.status).to.equal(400) - done() - }) - }) - }) - - describe('newCertificate()', () => { - describe('in multi user mode', () => { - let multiUser = true - let store = new LDP({ root: testAccountsDir, idp: multiUser }) - - it('should throw a 400 error if spkac param is missing', done => { - let options = { host, store, multiUser, authMethod: 'tls' } - let accountManager = AccountManager.from(options) - - let req = { - body: { - webid: 'https://alice.example.com/#me' - }, - session: { userId: 'https://alice.example.com/#me' }, - get: () => { return 'https://example.com' } - } - let res = HttpMocks.createResponse() - - let newCertificate = api.newCertificate(accountManager) - - newCertificate(req, res, (err) => { - expect(err.status).to.equal(400) - expect(err.message).to.equal('Missing spkac parameter') - done() - }) - }) - }) - }) -})