# Auth is completed unless an additional auth is needed
if ( $require_2FA ) {
my $patron = Koha::Patrons->find({userid => $userid});
- if ( C4::Context->preference('TwoFactorAuthentication') eq "enforced"
- || $patron->auth_method eq 'two-factor' )
- {
+ if ( C4::Context->preference('TwoFactorAuthentication') eq "enforced" && $patron->auth_method eq 'password' ) {
+ $auth_state = 'setup-additional-auth-needed';
+ $session->param('waiting-for-2FA-setup', 1);
+ %info = ();# We remove the warnings/errors we may have set incorrectly before
+ } elsif ( $patron->auth_method eq 'two-factor' ) {
# Ask for the OTP token
$auth_state = 'additional-auth-needed';
$session->param('waiting-for-2FA', 1);
);
}
+ if ( $auth_state eq 'setup-additional-auth-needed' ) {
+ $template->param(
+ TwoFA_setup => 1,
+ );
+ }
+
if ( $type eq 'opac' ) {
require Koha::Virtualshelves;
my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
return ( "additional-auth-needed", $session )
if $session->param('waiting-for-2FA');
+ return ( "setup-additional-auth-needed", $session )
+ if $session->param('waiting-for-2FA-setup');
+
return ( "ok", $session );
} else {
$session->delete();
my $secret32 = $params->{secret32};
my $secret = $params->{secret};
+ # FIXME Raise an exception if the syspref is disabled
+
Koha::Exceptions::MissingParameter->throw("Mandatory patron parameter missing")
unless $patron && ref($patron) eq 'Koha::Patron';
Koha::Exceptions::Authentication::Required->throw(
error => 'Authentication failure.' );
}
+ }
+ elsif ( $c->req->url->to_abs->path eq '/api/v1/auth/two-factor/registration'
+ || $c->req->url->to_abs->path eq '/api/v1/auth/two-factor/registration/verification' ) {
+
+ if ( $status eq 'setup-additional-auth-needed' ) {
+ $user = Koha::Patrons->find( $session->param('number') );
+ $cookie_auth = 1;
+ }
+ elsif ( $status eq 'ok' ) {
+ $user = Koha::Patrons->find( $session->param('number') );
+ if ( $user->auth_method ne 'password' ) {
+ # If the user already enabled 2FA they don't need to register again
+ Koha::Exceptions::Authentication->throw(
+ error => 'Cannot request this route.' );
+ }
+ $cookie_auth = 1;
+ }
+ else {
+ Koha::Exceptions::Authentication::Required->throw(
+ error => 'Authentication failure.' );
+ }
+
} else {
if ($status eq "ok") {
$user = Koha::Patrons->find( $session->param('number') );
}
+=head3 registration
+
+Ask for a registration secret. It will return a QR code image and a secret32.
+
+The secret must be sent back to the server with the pin code for the verification step.
+
+=cut
+
+sub registration {
+
+ my $c = shift->openapi->valid_input or return;
+
+ my $patron = Koha::Patrons->find( $c->stash('koha.user')->borrowernumber );
+
+ return try {
+ my $secret = Koha::AuthUtils::generate_salt( 'weak', 16 );
+ my $auth = Koha::Auth::TwoFactorAuth->new(
+ { patron => $patron, secret => $secret } );
+
+ my $response = {
+ issuer => $auth->issuer,
+ key_id => $auth->key_id,
+ qr_code => $auth->qr_code,
+ secret32 => $auth->secret32,
+
+ # IMPORTANT: get secret32 after qr_code call !
+ };
+ $auth->clear;
+
+ return $c->render(status => 201, openapi => $response);
+ }
+ catch {
+ $c->unhandled_exception($_);
+ };
+
+}
+
+=head3 verification
+
+Verify the registration, get the pin code and the secret retrieved from the registration.
+
+The 2FA_ENABLE notice will be generated if the pin code is correct, and the patron will have their two-factor authentication setup completed.
+
+=cut
+
+sub verification {
+
+ my $c = shift->openapi->valid_input or return;
+
+ my $patron = Koha::Patrons->find( $c->stash('koha.user')->borrowernumber );
+
+ return try {
+
+ my $pin_code = $c->validation->param('pin_code');
+ my $secret32 = $c->validation->param('secret32');
+
+ my $auth = Koha::Auth::TwoFactorAuth->new(
+ { patron => $patron, secret32 => $secret32 } );
+
+ my $verified = $auth->verify(
+ $pin_code,
+ 1, # range
+ $secret32,
+ undef, # timestamp (defaults to now)
+ 30, # interval (default 30)
+ );
+
+ unless ($verified) {
+ return $c->render(
+ status => 400,
+ openapi => { error => "Invalid pin" }
+ );
+ }
+
+ # FIXME Generate a (new?) secret
+ $patron->encode_secret($secret32);
+ $patron->auth_method('two-factor')->store;
+ if ( $patron->notice_email_address ) {
+ $patron->queue_notice(
+ {
+ letter_params => {
+ module => 'members',
+ letter_code => '2FA_ENABLE',
+ branchcode => $patron->branchcode,
+ lang => $patron->lang,
+ tables => {
+ branches => $patron->branchcode,
+ borrowers => $patron->id
+ },
+ },
+ message_transports => ['email'],
+ }
+ );
+ }
+
+ return $c->render(status => 204, openapi => {});
+ }
+ catch {
+ $c->unhandled_exception($_);
+ };
+
+}
+
1;
x-koha-authorization:
permissions:
catalogue: "1"
+
+/auth/two-factor/registration:
+ post:
+ x-mojo-to: TwoFactorAuth#registration
+ operationId: Two factor register
+ tags:
+ - 2fa
+ summary: Generate a secret
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: OK
+ schema:
+ type: object
+ properties:
+ secret32:
+ type: string
+ qr_code:
+ type: string
+ issuer:
+ type: string
+ key_id:
+ type: string
+ additionalProperties: false
+ "400":
+ description: Bad Request
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ "403":
+ description: Access forbidden
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ "500":
+ description: |
+ Internal server error. Possible `error_code` attribute values:
+
+ * `internal_server_error`
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ x-koha-authorization:
+ permissions:
+ catalogue: "1"
+
+/auth/two-factor/registration/verification:
+ post:
+ x-mojo-to: TwoFactorAuth#verification
+ operationId: Two factor register verification
+ tags:
+ - 2fa
+ summary: Verify two-factor registration
+ parameters:
+ - name: secret32
+ in: formData
+ description: the secret
+ required: true
+ type: string
+ - name: pin_code
+ in: formData
+ description: the pin code
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "204":
+ description: OK
+ "401":
+ description: Authentication required
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ "400":
+ description: Bad Request
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ "403":
+ description: Access forbidden
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ "500":
+ description: |
+ Internal server error. Possible `error_code` attribute values:
+
+ * `internal_server_error`
+ schema:
+ $ref: "../swagger.yaml#/definitions/error"
+ x-koha-authorization:
+ permissions:
+ catalogue: "1"
$ref: "./paths/article_requests.yaml#/~1article_requests~1{article_request_id}"
/auth/otp/token_delivery:
$ref: paths/auth.yaml#/~1auth~1otp~1token_delivery
+ /auth/two-factor/registration:
+ $ref: paths/auth.yaml#/~1auth~1two-factor~1registration
+ /auth/two-factor/registration/verification:
+ $ref: paths/auth.yaml#/~1auth~1two-factor~1registration~1verification
"/biblios/{biblio_id}":
$ref: "./paths/biblios.yaml#/~1biblios~1{biblio_id}"
"/biblios/{biblio_id}/checkouts":
[% IF ( nopermission ) %]Access denied[% END %] › Koha
</title>
[% INCLUDE 'doc-head-close.inc' %]
+[% PROCESS 'auth-two-factor.inc' %]
</head>
<body id="main_auth" class="main_main-auth">
<p><a href="[% shibbolethLoginUrl | $raw %]">Log in using a Shibboleth account</a>.</p>
[% END %]
-[% IF !TwoFA_prompt && !Koha.Preference('staffShibOnly') %]
+[% IF !TwoFA_prompt && !TwoFA_setup && !Koha.Preference('staffShibOnly') %]
<!-- login prompt time-->
<form action="[% script_name | html %]" method="post" name="loginform" id="loginform">
<input type="hidden" name="koha_login_context" value="intranet" />
</p>
</form>
+[% ELSIF TwoFA_setup %]
+ [% PROCESS registration_form %]
[% END %]
[% IF ( nopermission ) %]
});
[% END %]
});
+
+ if( $("#registration-form").length ) {
+ $.ajax({
+ data: {},
+ type: 'POST',
+ url: '/api/v1/auth/two-factor/registration',
+ success: function (data) {
+ $("#qr_code").attr('src', data.qr_code);
+ $("#secret32").val(data.secret32);
+ $("#issuer").html(data.issuer);
+ $("#key_id").html(data.key_id);
+ $("#registration-form").show();
+ },
+ error: function (data) {
+ alert(data);
+ },
+ });
+ };
+
+ $("#register-2FA").on("click", function(e){
+ e.preventDefault();
+ const data = {
+ secret32: $("#secret32").val(),
+ pin_code: $("#pin_code").val(),
+ };
+ if (!data.pin_code) return;
+
+ $.ajax({
+ data: data,
+ type: 'POST',
+ url: '/api/v1/auth/two-factor/registration/verification',
+ success: function (data) {
+ alert(_("Two-factor authentication correctly configured. You will be redirected to the login screen."));
+ window.location = "/cgi-bin/koha/mainpage.pl";
+ },
+ error: function (data) {
+ const error = data.responseJSON.error;
+ if ( error == 'Invalid pin' ) {
+ $("#errors").html(_("Invalid PIN code")).show();
+ } else {
+ alert(error);
+ }
+ },
+ });
+ });
+
});
</script>
[% END %]
[% WRAPPER 'header.inc' %]
[% INCLUDE 'patron-search-header.inc' %]
[% END %]
+[% PROCESS 'auth-two-factor.inc' %]
[% WRAPPER 'sub-header.inc' %]
<nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb">
</p>
</div>
[% ELSE %]
+ [% PROCESS registration_form %]
- [% IF op == 'register' %]
- <div class="dialog message">
- <p>We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device.</p>
- <p>Can't scan the code?</p>
- <p>To add the entry manually, provide the following details to the application on your phone.</p>
- <p>Account: [% issuer | html %]</p>
- <p>Key: [% key_id | html %]</p>
- <p>Time based: Yes</p>
- </div>
-
- [% IF invalid_pin %]
- <div class="dialog error">Invalid PIN code</div>
- [% END %]
- <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
- <fieldset class="rows">
- <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
- <input type="hidden" name="op" value="register-2FA" />
- <input type="hidden" name="secret32" value="[% secret32 | html %]" />
- <ol>
- <li>
- <label for="qr_code">QR code: </label>
- <img id="qr_code" src="[% qr_code | $raw %]" />
- </li>
- <li>
- <label for="pin_code">PIN code: </label>
- <input type="text" id="pin_code" name="pin_code" value="" />
- </li>
- </ol>
- </fieldset>
- <fieldset class="action">
- <input type="submit" value="Register with two-factor app" />
- <a class="cancel" href="/cgi-bin/koha/members/two_factor_auth.pl">Cancel</a>
- </fieldset>
- </form>
- [% ELSE %]
+ <div id="registration-status">
[% IF patron.auth_method == "two-factor" %]
+ <div id="registration-status-enabled">
+ [% ELSE %]
+ <div id="registration-status-enabled" style="display: none;">
+ [% END %]
<div class="two-factor-status">Status: Enabled</div>
<form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
<input type="hidden" name="op" value="disable-2FA" />
<input type="submit" value="Disable two-factor authentication" />
</form>
+ </div>
+
+ [% IF patron.auth_method == "password" %]
+ <div id="registration-status-disabled">
[% ELSE %]
+ <div id="registration-status-disabled" style="display: none;">
+ [% END %]
<div class="two-factor-status">Status: Disabled</div>
+ [% IF Koha.Preference('TwoFactorAuthentication') == 'enforced' %]
+ <div>Two-factor authentication is mandatory to login. If you do not enable now it will be asked at your next login.</div>
+ [% END %]
- <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
- <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
- <input type="hidden" name="op" value="enable-2FA" />
- <input type="submit" value="Enable two-factor authentication" />
- </form>
-
- [% END %]
- [% END %]
+ <input id="enable-2FA" type="submit" value="Enable two-factor authentication" />
+ </div>
+ </div>
[% END %]
</main>
</div> <!-- /.col-sm-10.col-sm-push-2 -->
[% Asset.js("js/members-menu.js") | $raw %]
<script>
$(document).ready(function(){
- $(".delete").on("click", function(e){
- return confirmDelete(_("Are you sure you want to delete this key?"));
+ $("#enable-2FA").on("click", function(e){
+ e.preventDefault();
+ $.ajax({
+ data: {},
+ type: 'POST',
+ url: '/api/v1/auth/two-factor/registration',
+ success: function (data) {
+ $("#qr_code").attr('src', data.qr_code);
+ $("#secret32").val(data.secret32);
+ $("#issuer").html(data.issuer);
+ $("#key_id").html(data.key_id);
+ $("#registration-form").show();
+ $("#registration-status").hide();
+ },
+ error: function (data) {
+ alert(data);
+ },
+ });
});
+
+ $("#register-2FA").on("click", function(e){
+ e.preventDefault();
+ const data = {
+ secret32: $("#secret32").val(),
+ pin_code: $("#pin_code").val(),
+ };
+ if (!data.pin_code) return;
+
+ $.ajax({
+ data: data,
+ type: 'POST',
+ url: '/api/v1/auth/two-factor/registration/verification',
+ success: function (data) {
+ window.location = "/cgi-bin/koha/members/two_factor_auth.pl";
+ },
+ error: function (data) {
+ const error = data.responseJSON.error;
+ if ( error == 'Invalid pin' ) {
+ $("#errors").html(_("Invalid PIN code")).show();
+ } else {
+ alert(error);
+ }
+ },
+ });
+ });
+
});
</script>
[% END %]
token => scalar $cgi->param('csrf_token'),
};
- if ( $op eq 'register-2FA' ) {
- output_and_exit( $cgi, $cookie, $template, 'wrong_csrf_token' )
- unless Koha::Token->new->check_csrf($csrf_pars);
-
- my $pin_code = $cgi->param('pin_code');
- my $secret32 = $cgi->param('secret32');
- my $auth = Koha::Auth::TwoFactorAuth->new(
- { patron => $logged_in_user, secret32 => $secret32 } );
-
- my $verified = $auth->verify(
- $pin_code,
- 1, # range
- $secret32,
- undef, # timestamp (defaults to now)
- 30, # interval (default 30)
- );
-
- if ($verified) {
-
- # FIXME Generate a (new?) secret
- $logged_in_user->encode_secret($secret32);
- $logged_in_user->auth_method('two-factor')->store;
- $op = 'registered';
- if ( $logged_in_user->notice_email_address ) {
- $logged_in_user->queue_notice(
- {
- letter_params => {
- module => 'members',
- letter_code => '2FA_ENABLE',
- branchcode => $logged_in_user->branchcode,
- lang => $logged_in_user->lang,
- tables => {
- branches => $logged_in_user->branchcode,
- borrowers => $logged_in_user->id
- },
- },
- message_transports => ['email'],
- }
- );
- }
- }
- else {
- $template->param( invalid_pin => 1, );
- $op = 'enable-2FA';
- }
- }
-
- if ( $op eq 'enable-2FA' ) {
- my $secret = Koha::AuthUtils::generate_salt( 'weak', 16 );
- my $auth = Koha::Auth::TwoFactorAuth->new(
- { patron => $logged_in_user, secret => $secret } );
-
- $template->param(
- issuer => $auth->issuer,
- key_id => $auth->key_id,
- qr_code => $auth->qr_code,
- secret32 => $auth->secret32,
-
- # IMPORTANT: get secret32 after qr_code call !
- );
- $auth->clear;
- $op = 'register';
- }
- elsif ( $op eq 'disable-2FA' ) {
+ if ( $op eq 'disable-2FA' ) {
output_and_exit( $cgi, $cookie, $template, 'wrong_csrf_token' )
unless Koha::Token->new->check_csrf($csrf_pars);
my $auth =