You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PasswordLessLoginForm.vue 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <template>
  2. <form v-if="isHttps && hasPublicKeyCredential"
  3. ref="loginForm"
  4. method="post"
  5. name="login"
  6. @submit.prevent="submit">
  7. <fieldset>
  8. <p class="grouptop groupbottom">
  9. <input id="user"
  10. ref="user"
  11. v-model="user"
  12. type="text"
  13. name="user"
  14. :autocomplete="autoCompleteAllowed ? 'on' : 'off'"
  15. :placeholder="t('core', 'Username or email')"
  16. :aria-label="t('core', 'Username or email')"
  17. required
  18. @change="$emit('update:username', user)">
  19. <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
  20. </p>
  21. <div v-if="!validCredentials">
  22. {{ t('core', 'Your account is not setup for passwordless login.') }}
  23. </div>
  24. <LoginButton v-if="validCredentials"
  25. :loading="loading"
  26. :inverted-colors="invertedColors"
  27. @click="authenticate" />
  28. </fieldset>
  29. </form>
  30. <div v-else-if="!hasPublicKeyCredential">
  31. {{ t('core', 'Passwordless authentication is not supported in your browser.')}}
  32. </div>
  33. <div v-else-if="!isHttps">
  34. {{ t('core', 'Passwordless authentication is only available over a secure connection.')}}
  35. </div>
  36. </template>
  37. <script>
  38. import {
  39. startAuthentication,
  40. finishAuthentication,
  41. } from '../../service/WebAuthnAuthenticationService'
  42. import LoginButton from './LoginButton'
  43. class NoValidCredentials extends Error {
  44. }
  45. export default {
  46. name: 'PasswordLessLoginForm',
  47. components: {
  48. LoginButton,
  49. },
  50. props: {
  51. username: {
  52. type: String,
  53. default: '',
  54. },
  55. redirectUrl: {
  56. type: String,
  57. },
  58. invertedColors: {
  59. type: Boolean,
  60. default: false,
  61. },
  62. autoCompleteAllowed: {
  63. type: Boolean,
  64. default: true,
  65. },
  66. isHttps: {
  67. type: Boolean,
  68. default: false,
  69. },
  70. hasPublicKeyCredential: {
  71. type: Boolean,
  72. default: false,
  73. }
  74. },
  75. data() {
  76. return {
  77. user: this.username,
  78. loading: false,
  79. validCredentials: true,
  80. }
  81. },
  82. methods: {
  83. authenticate() {
  84. console.debug('passwordless login initiated')
  85. this.getAuthenticationData(this.user)
  86. .then(publicKey => {
  87. console.debug(publicKey)
  88. return publicKey
  89. })
  90. .then(this.sign)
  91. .then(this.completeAuthentication)
  92. .catch(error => {
  93. if (error instanceof NoValidCredentials) {
  94. this.validCredentials = false
  95. return
  96. }
  97. console.debug(error)
  98. })
  99. },
  100. getAuthenticationData(uid) {
  101. const base64urlDecode = function(input) {
  102. // Replace non-url compatible chars with base64 standard chars
  103. input = input
  104. .replace(/-/g, '+')
  105. .replace(/_/g, '/')
  106. // Pad out with standard base64 required padding characters
  107. const pad = input.length % 4
  108. if (pad) {
  109. if (pad === 1) {
  110. throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
  111. }
  112. input += new Array(5 - pad).join('=')
  113. }
  114. return window.atob(input)
  115. }
  116. return startAuthentication(uid)
  117. .then(publicKey => {
  118. console.debug('Obtained PublicKeyCredentialRequestOptions')
  119. console.debug(publicKey)
  120. if (!Object.prototype.hasOwnProperty.call(publicKey, 'allowCredentials')) {
  121. console.debug('No credentials found.')
  122. throw new NoValidCredentials()
  123. }
  124. publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
  125. publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
  126. return {
  127. ...data,
  128. 'id': Uint8Array.from(base64urlDecode(data.id), c => c.charCodeAt(0)),
  129. }
  130. })
  131. console.debug('Converted PublicKeyCredentialRequestOptions')
  132. console.debug(publicKey)
  133. return publicKey
  134. })
  135. .catch(error => {
  136. console.debug('Error while obtaining data')
  137. throw error
  138. })
  139. },
  140. sign(publicKey) {
  141. const arrayToBase64String = function(a) {
  142. return window.btoa(String.fromCharCode(...a))
  143. }
  144. return navigator.credentials.get({ publicKey })
  145. .then(data => {
  146. console.debug(data)
  147. console.debug(new Uint8Array(data.rawId))
  148. console.debug(arrayToBase64String(new Uint8Array(data.rawId)))
  149. return {
  150. id: data.id,
  151. type: data.type,
  152. rawId: arrayToBase64String(new Uint8Array(data.rawId)),
  153. response: {
  154. authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
  155. clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
  156. signature: arrayToBase64String(new Uint8Array(data.response.signature)),
  157. userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null,
  158. },
  159. }
  160. })
  161. .then(challenge => {
  162. console.debug(challenge)
  163. return challenge
  164. })
  165. .catch(error => {
  166. console.debug('GOT AN ERROR!')
  167. console.debug(error) // Example: timeout, interaction refused...
  168. })
  169. },
  170. completeAuthentication(challenge) {
  171. console.debug('TIME TO COMPLETE')
  172. const location = this.redirectUrl
  173. return finishAuthentication(JSON.stringify(challenge))
  174. .then(data => {
  175. console.debug('Logged in redirecting')
  176. window.location.href = location
  177. })
  178. .catch(error => {
  179. console.debug('GOT AN ERROR WHILE SUBMITTING CHALLENGE!')
  180. console.debug(error) // Example: timeout, interaction refused...
  181. })
  182. },
  183. submit() {
  184. // noop
  185. },
  186. },
  187. }
  188. </script>
  189. <style scoped>
  190. </style>