From 91a70eac8c9d239f395b3b80ba7bac4fe49bcfc5 Mon Sep 17 00:00:00 2001
From: Shayna Atkinson
Date: Fri, 28 Jul 2023 10:56:27 -0400
Subject: [PATCH 1/3] Add capability to generate, print, and delete Privacy
Idea backup codes as an authenticator for a CO Person
---
.../Config/Schema/schema.xml | 32 +++
.../Controller/PaperTokensController.php | 198 ++++++++++++++++++
.../Controller/PrivacyIdeasController.php | 3 +-
.../PrivacyIdeaAuthenticator/Lib/enum.php | 2 +
.../PrivacyIdeaAuthenticator/Lib/lang.php | 14 +-
.../Model/PaperToken.php | 67 ++++++
.../Model/PrivacyIdea.php | 56 +++--
.../Model/PrivacyIdeaAuthenticator.php | 154 ++++++++------
.../View/PaperTokens/fields.inc | 123 +++++++++++
.../View/PaperTokens/generate.ctp | 118 +++++++++++
.../View/PaperTokens/index.ctp | 125 +++++++++++
app/Lib/lang.php | 1 +
.../Model/CoLdapProvisionerTarget.php | 12 +-
app/webroot/css/co-base.css | 6 +
14 files changed, 831 insertions(+), 80 deletions(-)
create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php
create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc
create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
create mode 100644 app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml
index ad6d648af..4892f3fcb 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Config/Schema/schema.xml
@@ -86,4 +86,36 @@
totp_token_id
+
+
+
+
+
+
+
+ REFERENCES cm_privacy_idea_authenticators(id)
+
+
+ REFERENCES cm_co_people(id)
+
+
+
+
+
+ REFERENCES cm_paper_tokens(id)
+
+
+
+
+
+
+ co_person_id
+
+
+ serial
+
+
+ paper_token_id
+
+
\ No newline at end of file
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
new file mode 100644
index 000000000..1407f7b1b
--- /dev/null
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
@@ -0,0 +1,198 @@
+ array('Server')
+ );
+
+ /**
+ * Add a Standard Object.
+ *
+ * @since COmanage Registry v4.3.0
+ */
+
+ public function generate() {
+ if($this->request->is('get')) {
+ parent::add();
+
+ //$this->set('title_for_layout', 'Generated '.$this->viewVars['vv_authenticator']['Authenticator']['description']);
+ $this->set('title_for_layout', 'Generated Backup Codes');
+
+ if(!empty($this->request->params['named']['onFinish'])) {
+ $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']);
+ }
+
+ try {
+ $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'],
+ $this->viewVars['vv_co_person']['CoPerson']['id']);
+
+ $this->set('vv_token_info', $tokenInfo);
+
+ $newdata = array(
+ 'PaperToken' => array(
+ 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'],
+ 'serial' => $tokenInfo['serial']
+ )
+ );
+
+ $this->generateHistory('generate', $newdata, array());
+
+ if(!empty($tokenInfo['otps'])) {
+ $this->set('vv_otps', (array)$tokenInfo['otps']);
+ debug($vv_otps);
+ }
+ }
+ catch(Exception $e) {
+ $this->Flash->set($e->getMessage(), array('key' => 'error'));
+ }
+ }
+ }
+
+ /**
+ * Callback before other controller methods are invoked or views are rendered.
+ *
+ * @since COmanage Registry v4.3.0
+ */
+
+ public function beforeFilter() {
+ // We operate as a virtual controller, and tweak the settings to pull
+ // records for the token type that this instantiation is configured for
+
+ $this->uses[] = 'PrivacyIdeaAuthenticator.PrivacyIdea';
+
+ parent::beforeFilter();
+ }
+
+ /**
+ * Perform any dependency checks required prior to a delete operation.
+ * This method is intended to be overridden by model-specific controllers.
+ * - postcondition: Session flash message updated (HTML) or HTTP status returned (REST)
+ *
+ * @since COmanage Registry v4.3.0
+ * @param Array Current data
+ * @return boolean true if dependency checks succeed, false otherwise.
+ */
+
+ function checkDeleteDependencies($curdata) {
+ // Remove the Token from the server. This should really happen in
+ // PaperToken::beforeDelete(), but for some reason (probably some weird
+ // namespace management issue) that callback isn't being called. It's also
+ // slightly easier to make the API call from the controller (using the
+ // PrivacyIdea model). Ultimately, this will need to be rewritten for
+ // Cake 4.
+
+ // We need the Serial ID, not the token ID
+
+ if(empty($curdata['PaperToken']['serial'])) {
+ throw new InvalidArgumentException(_txt('er.notprov.id', array(_txt('pl.privacyideaauthenticator.fd.serial'))));
+ }
+
+ $this->PrivacyIdea->deleteToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'],
+ $curdata['PaperToken']['serial']);
+
+ return true;
+ }
+
+ /**
+ * Generate history records for a transaction. This method is intended to be
+ * overridden by model-specific controllers, and will be called from within a
+ * try{} block so that HistoryRecord->record() may be called without worrying
+ * about catching exceptions.
+ *
+ * @since COmanage Registry v4.3.0
+ * @param String Controller action causing the change
+ * @param Array Data provided as part of the action (for add/edit)
+ * @param Array Previous data (for delete/edit)
+ * @return boolean Whether the function completed successfully (which does not necessarily imply history was recorded)
+ */
+
+ public function generateHistory($action, $newdata, $olddata) {
+ // Build a change string
+ $cstr = "";
+ $cop = null;
+ $act = null;
+
+ switch($action) {
+ case 'generate':
+ $cstr = _txt('rs.generated-a2', array(_txt('ct.paper_tokens.1'), $newdata['PaperToken']['serial']));
+ $cop = $newdata['PaperToken']['co_person_id'];
+ $act = PrivacyIDEActionEnum::TokenGenerated;
+ break;
+ case 'delete':
+ $cstr = _txt('rs.deleted-a2', array(_txt('ct.paper_tokens.1'), $olddata['PaperToken']['serial']));
+ $cop = $olddata['PaperToken']['co_person_id'];
+ $act = PrivacyIDEActionEnum::TokenDeleted;
+ break;
+ }
+
+ $this->Co->CoPerson->HistoryRecord->record($cop,
+ null,
+ null,
+ $this->Session->read('Auth.User.co_person_id'),
+ $act,
+ $cstr);
+
+ return true;
+ }
+
+ /**
+ * Authorization for this Controller, called by Auth component
+ * - precondition: Session.Auth holds data used for authz decisions
+ * - postcondition: $permissions set with calculated permissions
+ *
+ * @since COmanage Registry v4.3.0
+ * @return Array Permissions
+ */
+
+ function isAuthorized() {
+ $roles = $this->Role->calculateCMRoles();
+
+ // Construct the permission set for this user, which will also be passed to the view.
+ $p = array();
+
+ // Determine what operations this user can perform
+
+ // Merge in the permissions calculated by our parent
+ $p = array_merge($p, $this->calculateParentPermissions(true));
+
+ // Tokens can't be edited, only deleted
+ $p['edit'] = false;
+
+ $p['generate'] = isset($p['manage']) ? $p['manage'] : false;
+
+ $this->set('permissions', $p);
+ return($p[$this->action]);
+ }
+}
\ No newline at end of file
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php
index 1647bd705..a951cb0b2 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PrivacyIdeasController.php
@@ -32,7 +32,8 @@ class PrivacyIdeasController extends SAMController {
public $name = "PrivacyIdeas";
protected $pi_token_models = array(
- PrivacyIDEATokenTypeEnum::TOTP => 'TotpToken'
+ PrivacyIDEATokenTypeEnum::TOTP => 'TotpToken',
+ PrivacyIDEATokenTypeEnum::Paper => 'PaperToken'
);
/**
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php
index e6dd30e2f..a2c2b0cd6 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/enum.php
@@ -31,9 +31,11 @@ class PrivacyIDEActionEnum
const TokenConfirmed = 'MFAC';
const TokenDeleted = 'MFAD';
const TokenEdited = 'MFAE';
+ const TokenGenerated = 'MFAG';
}
class PrivacyIDEATokenTypeEnum
{
const TOTP = 'TO';
+ const Paper = 'PP';
}
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
index e41bd9246..8eb162970 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
@@ -39,6 +39,8 @@
'ct.privacy_ideas.pl' => 'PrivacyIDEA Tokens',
'ct.totp_tokens.1' => 'TOTP Token',
'ct.totp_tokens.pl' => 'TOTP Tokens',
+ 'ct.paper_tokens.1' => 'Backup Codes Token',
+ 'ct.paper_tokens.pl' => 'Backup Codes Tokens',
// Enumerations
'pl.privacyideaauthenticator.en.action' => array(
@@ -49,7 +51,8 @@
),
'en.privacyideaauthenticator.token_type' => array(
- PrivacyIDEATokenTypeEnum::TOTP => 'Time-Based OTP (TOTP)'
+ PrivacyIDEATokenTypeEnum::TOTP => 'Time-Based OTP (TOTP)',
+ PrivacyIDEATokenTypeEnum::Paper => 'Backup Codes'
),
// Error messages
@@ -58,13 +61,18 @@
// Plugin texts
'pl.privacyideaauthenticator.alt.google' => 'QR Code for Google Authenticator',
+ 'pl.privacyideaauthenticator.alt.paper' => 'Paper token list',
'pl.privacyideaauthenticator.fd.identifier_type' => 'Identifier Type',
'pl.privacyideaauthenticator.fd.realm' => 'PrivacyIDEA Realm',
'pl.privacyideaauthenticator.fd.serial' => 'Serial',
'pl.privacyideaauthenticator.fd.token_type' => 'Token Type',
'pl.privacyideaauthenticator.fd.validation_server' => 'Validation API Server',
- 'pl.privacyideaauthenticator.status' => '%1$s token(s) registered, %2$s confirmed',
+ 'pl.privacyideaauthenticator.totpstatus' => '%1$s token(s) registered, %2$s confirmed',
+ 'pl.privacyideaauthenticator.paperstatus' => '%1$s token(s) registered',
'pl.privacyideaauthenticator.token.confirmed' => 'Token Confirmed',
'pl.privacyideaauthenticator.totp.step1' => 'First, scan the QR Code to add this token to Google Authenticator',
- 'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm'
+ 'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm',
+ 'pl.privacyideaauthenticator.paper.intro' => 'Use the backup codes in order, one after the other. Mark off used values.',
+ 'pl.privacyideaauthenticator.paper.caution' => 'Backup codes are a weak second factor. Please assure no one has access to these values. Store them in a safe location',
+ 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.'
);
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php
new file mode 100644
index 000000000..1be5a86d5
--- /dev/null
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PaperToken.php
@@ -0,0 +1,67 @@
+ array('priority' => 5),
+ 'Provisioner');
+
+ // Association rules from this model to other models
+ public $belongsTo = array(
+ "PrivacyIdeaAuthenticator.PrivacyIdeaAuthenticator",
+ "CoPerson"
+ );
+
+ // Default display field for cake generated views
+ public $displayField = "password_type";
+
+ // Validation rules for table elements
+ public $validate = array(
+ 'privacy_idea_authenticator_id' => array(
+ 'rule' => 'numeric',
+ 'required' => true,
+ 'allowEmpty' => false
+ ),
+ 'co_person_id' => array(
+ 'rule' => 'numeric',
+ 'required' => true,
+ 'allowEmpty' => false
+ ),
+ 'serial' => array(
+ 'rule' => 'notBlank',
+ 'required' => true,
+ 'allowEmpty' => false
+ )
+ );
+}
\ No newline at end of file
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
index 20582f657..ffbffff0d 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
@@ -28,6 +28,7 @@
App::uses("Identifier", "Model");
App::uses("Server", "Model");
App::uses("TotpToken", "PrivacyIdeaAuthenticator.Model");
+App::uses("PaperToken", "PrivacyIdeaAuthenticator.Model");
class PrivacyIdea extends AppModel {
// Define class name for cake
@@ -164,18 +165,27 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) {
$identifier = $this->lookupIdentifier($privacyIdeaAuthenticator['identifier_type'], $coPersonId);
$Http = $this->connect($privacyIdeaAuthenticator['server_id']);
-
- // XXX For now we only set params for TOTP tokens. We'll need to refactor
- // this section when we add support for additional token types.
-
+
+ $token_type = $privacyIdeaAuthenticator['token_type'];
+
$params = array(
- 'type' => 'totp',
'user' => $identifier,
'realm' => $privacyIdeaAuthenticator['realm'],
'genkey' => '1',
- 'optlen' => '6'
);
-
+
+ switch ($token_type) {
+ case PrivacyIDEATokenTypeEnum::TOTP:
+ $params['type'] = 'totp';
+ $params['otplen'] = '6';
+ break;
+
+ case PrivacyIDEATokenTypeEnum::Paper:
+ $params['type'] = 'paper';
+ $params['otplen'] = '8';
+ break;
+ }
+
$response = $Http->post("/token/init", $params, $this->requestCfg);
$jresponse = json_decode($response);
@@ -188,15 +198,31 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) {
'privacy_idea_authenticator_id' => $privacyIdeaAuthenticator['id'],
'co_person_id' => $coPersonId,
'serial' => $jresponse->detail->serial,
- 'confirmed' => false
);
-
- $TotpToken = new TotpToken();
- $TotpToken->save($token);
-
- // We don't persist the QR Data, but we do need to return it for rendering
- $token['qr_data'] = $jresponse->detail->googleurl->img;
-
+
+ switch ($token_type) {
+ case PrivacyIDEATokenTypeEnum::TOTP:
+ $token['confirmed'] = false;
+ $TotpToken = new TotpToken();
+ $TotpToken->save($token);
+
+ // We don't persist the QR Data, but we do need to return it for rendering
+ $token['qr_data'] = $jresponse->detail->googleurl->img;
+ break;
+
+ case PrivacyIDEATokenTypeEnum::Paper:
+ $PaperToken = new PaperToken();
+ $PaperToken->save($token);
+
+ // We don't persist the codes themselves but need to present them to the user for copying/printing
+ $token['otps'] = (array)$jresponse->detail->otps;
+ break;
+ }
+
+ if(!$jresponse->result->status) {
+ throw new RuntimeException($jresponse->result->error->message);
+ }
+
return $token;
}
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
index 3f6b153d6..1819cdca3 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
@@ -35,27 +35,28 @@ class PrivacyIdeaAuthenticator extends AuthenticatorBackend {
// Required by COmanage Plugins
public $cmPluginType = "authenticator";
- // Add behaviors
+ // Add behaviors
public $actsAs = array('Containable');
// Document foreign keys
public $cmPluginHasMany = array(
- "CoPerson" => array("TotpToken")
- );
+ "CoPerson" => array("TotpToken", "PaperToken"),
+ );
- // Association rules from this model to other models
- public $belongsTo = array(
- "Authenticator",
+ // Association rules from this model to other models
+ public $belongsTo = array(
+ "Authenticator",
"Server",
"ValidationServer" => array(
'className' => 'Server',
'foreignKey' => 'validation_server_id'
)
- );
+ );
- public $hasMany = array(
- "PrivacyIdeaAuthenticator.TotpToken"
- );
+ public $hasMany = array(
+ "PrivacyIdeaAuthenticator.TotpToken",
+ "PrivacyIdeaAuthenticator.PaperToken"
+ );
// Default display field for cake generated views
public $displayField = "realm";
@@ -92,7 +93,7 @@ class PrivacyIdeaAuthenticator extends AuthenticatorBackend {
'allowEmpty' => false
),
'token_type' => array(
- 'rule' => array('inList', array(PrivacyIDEATokenTypeEnum::TOTP)),
+ 'rule' => array('inList', array(PrivacyIDEATokenTypeEnum::TOTP, PrivacyIDEATokenTypeEnum::Paper)),
'required' => true,
'allowEmpty' => false
),
@@ -125,7 +126,7 @@ public function cmPluginMenus() {
return array();
}
- /**
+ /**
* Obtain current data suitable for passing to manage() and provisioners.
*
* @since COmanage Registry v4.0.0
@@ -134,16 +135,27 @@ public function cmPluginMenus() {
* @param integer $coPersonId CO Person ID
* @return Array As returned by find
* @throws RuntimeException
- */
+ */
+
+ public function current($id, $backendId, $coPersonId) {
- public function current($id, $backendId, $coPersonId) {
$args = array();
- $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $backendId;
+
$args['conditions']['TotpToken.co_person_id'] = $coPersonId;
+ $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $backendId;
$args['contain'] = false;
-
- return $this->TotpToken->find('all', $args);
- }
+ $results = $this->TotpToken->find('all', $args);
+
+ if(empty($results)) {
+ unset($args);
+ $args['conditions']['PaperToken.co_person_id'] = $coPersonId;
+ $args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $backendId;
+ $args['contain'] = false;
+ $results = $this->PaperToken->find('all', $args);
+ }
+
+ return $results;
+ }
/**
* Perform backend specific actions on a lock operation.
@@ -172,57 +184,79 @@ public function lock($id, $coPersonId) {
return true;
}
- /**
- * Reset Authenticator data for a CO Person.
- *
- * @since COmanage Registry v3.1.0
- * @param integer $coPersonId CO Person ID
- * @param integer $actorCoPersonId Actor CO Person ID
- * @return boolean true on success
- */
+ /**
+ * Reset Authenticator data for a CO Person.
+ *
+ * @since COmanage Registry v3.1.0
+ * @param integer $coPersonId CO Person ID
+ * @param integer $actorCoPersonId Actor CO Person ID
+ * @return boolean true on success
+ */
public function reset($coPersonId, $actorCoPersonId) {
- // It's not immediately obvious what we should do on a reset... so for now
+ // It's not immediately obvious what we should do on a reset... so for now
// we return false until we have better requirements.
- return false;
- }
+ return false;
+ }
- /**
- * Obtain the current Authenticator status for a CO Person.
- *
- * @since COmanage Registry v4.0.0
- * @param integer $coPersonId CO Person ID
- * @return Array Array with values
- * status: AuthenticatorStatusEnum
- * comment: Human readable string, visible to the CO Person
- */
+ /**
+ * Obtain the current Authenticator status for a CO Person.
+ *
+ * @since COmanage Registry v4.0.0
+ * @param integer $coPersonId CO Person ID
+ * @return Array Array with values
+ * status: AuthenticatorStatusEnum
+ * comment: Human readable string, visible to the CO Person
+ */
- public function status($coPersonId) {
- // We can have more than one Authenticator, but only of the type token_type.
- // For now, we only work with TotpTokens.
-
+ public function status($coPersonId) {
+
$status = AuthenticatorStatusEnum::NotSet;
$comment = _txt('fd.set.not');
-
+
+ $pcfg = $this->getConfig();
+
+ $token_type = $pcfg['PrivacyIdeaAuthenticator']['token_type'];
+ $piauth_id = $pcfg['PrivacyIdeaAuthenticator']['id'];
+
$args = array();
- $args['conditions']['TotpToken.co_person_id'] = $coPersonId;
- $args['contain'] = false;
-
- $tokens = $this->TotpToken->find('all', $args);
-
- if(count($tokens) > 0) {
- $confirmed = Hash::extract($tokens, '{n}.TotpToken[confirmed=true]');
-
- $status = AuthenticatorStatusEnum::Active;
- $comment = _txt('pl.privacyideaauthenticator.status', array(count($tokens), count($confirmed)));
+
+ switch ($token_type) {
+ case PrivacyIDEATokenTypeEnum::TOTP:
+ $args['conditions']['TotpToken.co_person_id'] = $coPersonId;
+ $args['conditions']['TotpToken.privacy_idea_authenticator_id'] = $piauth_id;
+ $args['contain'] = false;
+
+ $tokens = $this->TotpToken->find('all', $args);
+
+ if(count($tokens) > 0) {
+ $confirmed = Hash::extract($tokens, '{n}.TotpToken[confirmed=true]');
+
+ $status = AuthenticatorStatusEnum::Active;
+ $comment = _txt('pl.privacyideaauthenticator.totpstatus', array(count($tokens), count($confirmed)));
+ }
+ break;
+
+ case PrivacyIDEATokenTypeEnum::Paper:
+ $args['conditions']['PaperToken.co_person_id'] = $coPersonId;
+ $args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $piauth_id;
+ $args['contain'] = false;
+
+ $tokens = $this->PaperToken->find('all', $args);
+
+ if(count($tokens) > 0) {
+ $status = AuthenticatorStatusEnum::Active;
+ $comment = _txt('pl.privacyideaauthenticator.paperstatus', array(count($tokens)));
+ }
+ break;
}
-
- return array(
- 'status' => $status,
- 'comment' => $comment
- );
- }
-
+
+ return array(
+ 'status' => $status,
+ 'comment' => $comment
+ );
+ }
+
/**
* Perform backend specific actions on an unlock operation.
*
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc
new file mode 100644
index 000000000..924370312
--- /dev/null
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/fields.inc
@@ -0,0 +1,123 @@
+
+Form->hidden('privacy_idea_authenticator_id',
+ array('default' => $vv_authenticator['PrivacyIdeaAuthenticator']['id'])) . "\n";
+ print $this->Form->hidden('co_person_id', array('default' => $vv_co_person['CoPerson']['id'])) . "\n";
+
+ if(!empty($vv_token_info['otps'])) {
+ print $this->Form->hidden('otps', array('default' => $vv_token_info['otps']));
+ }
+
+ if(!empty($vv_token_info['serial'])) {
+ print $this->Form->hidden('serial', array('default' => $vv_token_info['serial']));
+ }
+
+ if(!empty($vv_on_finish_url)) {
+ print $this->Form->hidden('onFinish', array('default' => $vv_on_finish_url));
+ }
+
+ // Add breadcrumbs
+ print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea'));
+?>
+
+
+
+
+
+
+ info
+
+
+
+ warning
+
+
+
+
+
+
+
+ #
+ OTP
+
+
+
+ $otp): ?>
+
+
+
+
+
+
+
+
+
+ print backup codes
+action == 'view'): ?>
+
+
+
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
new file mode 100644
index 000000000..06fa5d16f
--- /dev/null
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
@@ -0,0 +1,118 @@
+
+request->params['named']['onFinish'])) {
+ $params['topLinks'][] = $this->Html->link(_txt('op.skip'),
+ urldecode($this->request->params['named']['onFinish']),
+ array('class' => 'forwardbutton'));
+ }
+ print $this->element("pageTitleAndButtons", $params);
+
+ // Add breadcrumbs
+ print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea'));
+?>
+
+
+
+
+
+
+ info
+
+
+
+ warning
+
+
+
+
+
+
+
+ #
+ OTP
+
+
+
+ $otp): ?>
+
+
+
+
+
+
+
+
+
+ print backup codes
+action == 'view'): ?>
+
+
+
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
new file mode 100644
index 000000000..b6cc885c9
--- /dev/null
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
@@ -0,0 +1,125 @@
+element("coCrumb", array('authenticator' => 'PrivacyIdea'));
+
+ // Add page title
+ $params = array();
+ $params['title'] = $title_for_layout;
+
+ // Add top links
+ $params['topLinks'] = array();
+
+ if($permissions['add']) {
+ $params['topLinks'][] = $this->Html->link(
+ _txt('op.generate', array($title_for_layout)),
+ array(
+ 'plugin' => 'privacy_idea_authenticator', // XXX can inflect from $vv_authenticator['Authenticator']['plugin']
+ 'controller' => 'paper_tokens',
+ 'action' => 'generate',
+ 'authenticatorid' => $vv_authenticator['Authenticator']['id'],
+ 'copersonid' => $vv_co_person['CoPerson']['id']
+ ),
+ array('class' => 'addbutton')
+ );
+ }
+
+ print $this->element("pageTitleAndButtons", $params);
+?>
+
+
+
+ Paginator->sort('token_type', _txt('pl.privacyideaauthenticator.fd.token_type')); ?>
+ Paginator->sort('serial', _txt('pl.privacyideaauthenticator.fd.serial')); ?>
+
+
+
+
+
+
+
+
+
+
+ Html->link(
+ // $t['PaperToken']['serial'],
+ // array(
+ // 'plugin' => 'privacy_idea_authenticator', // XXX can inflect from $vv_authenticator['Authenticator']['plugin']
+ // 'controller' => 'paper_tokens',
+ // 'action' => 'view',
+ // $t['PaperToken']['id']
+ // )
+ // );
+ print $t['PaperToken']['serial'];
+ ?>
+
+
+ Html->link(
+ // _txt('op.view'),
+ // array(
+ // 'plugin' => 'privacy_idea_authenticator',
+ // 'controller' => 'paper_tokens',
+ // 'action' => 'view',
+ // $t['PaperToken']['id']
+ // ),
+ // array('class' => 'viewbutton')
+ // ) . "\n";
+ // }
+
+ if($permissions['delete']) {
+ print ''
+ . _txt('op.delete')
+ . ' ';
+ }
+ ?>
+
+
+
+
+
+
+
+element("pagination");
diff --git a/app/Lib/lang.php b/app/Lib/lang.php
index 66775d709..62d092d0b 100644
--- a/app/Lib/lang.php
+++ b/app/Lib/lang.php
@@ -2424,6 +2424,7 @@
'rs.ev.ver-a' => 'Email Address %1$s verified by %2$s',
'rs.expunged' => 'Record Expunged',
'rs.found.cnt' => '%1$s Record(s) Found',
+ 'rs.generated-a2' => '%1$s "%2$s" Generated',
'rs.gr.added' => 'Added CO Group "%1$s"',
'rs.gr.deleted' => 'Deleted CO Group "%1$s"',
'rs.gr.reconcile.ok' => 'Reconciliation Successful',
diff --git a/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php b/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php
index 6705e26a2..a9398230d 100644
--- a/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php
+++ b/app/Plugin/LdapProvisioner/Model/CoLdapProvisionerTarget.php
@@ -220,6 +220,7 @@ protected function assembleAttributes($coProvisioningTargetData,
$dns,
$dnAttributes,
$uam) {
+
// First see if we're working with a Group record or a Person record
$person = isset($provisioningData['CoPerson']['id']);
$group = isset($provisioningData['CoGroup']['id']);
@@ -901,7 +902,16 @@ protected function assembleAttributes($coProvisioningTargetData,
}
}
}
-
+ if(!empty($provisioningData['PaperToken'])) {
+ foreach($provisioningData['PaperToken'] as $pt) {
+ if($attropts) {
+ $lrattr = $lattr . ";type-paper";
+ $attributes[$lrattr][] = $pt['serial'];
+ } else {
+ $attributes[$attr][] = $pt['serial'];
+ }
+ }
+ }
if(!$attropts && empty($attributes[$attr]) && !$modify) {
// This is the same as the approach using $found, but without an extra variable
unset($attributes[$attr]);
diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css
index a79e38ff8..9eae1b174 100644
--- a/app/webroot/css/co-base.css
+++ b/app/webroot/css/co-base.css
@@ -474,6 +474,12 @@ form#notificationStatus {
font-size: 1.6rem;
margin: 0 0.25em 0 0;
}
+#content .co-info-topbox.warn-level-a {
+ background-color: #fcc;
+}
+#content .co-info-topbox.warn-level-a .material-icons {
+ color: #c00;
+}
.material-icons.error {
color: #d00;
}
From 2d87d4ec33688c6e15ad08007e317b7ec53ca6ab Mon Sep 17 00:00:00 2001
From: Shayna Atkinson
Date: Fri, 8 Sep 2023 11:36:39 -0400
Subject: [PATCH 2/3] PR review changes; new dialog
---
.../Controller/PaperTokensController.php | 33 ++++--
.../PrivacyIdeaAuthenticator/Lib/lang.php | 5 +-
.../Model/PrivacyIdea.php | 14 ++-
.../Model/PrivacyIdeaAuthenticator.php | 2 +-
.../View/PaperTokens/generate.ctp | 100 +++++++++++-------
.../View/PaperTokens/index.ctp | 2 +-
app/Config/bootstrap.php | 4 +-
7 files changed, 99 insertions(+), 61 deletions(-)
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
index 1407f7b1b..fafb3cb06 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
@@ -21,7 +21,7 @@
*
* @link http://www.internet2.edu/comanage COmanage Project
* @package registry-plugin
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
@@ -38,16 +38,28 @@ class PaperTokensController extends SAMController {
);
/**
- * Add a Standard Object.
+ * Add action to be used when adding a PaperToke as part of an Enrollment Flow
*
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
+ */
+
+ public function add() {
+ $this->setAction('generate');
+ }
+
+ /**
+ * Generate a Paper Token (backup codes)
+ *
+ * @since COmanage Registry v4.4.0
*/
public function generate() {
- if($this->request->is('get')) {
+
+ if(!$this->request->is('get')) {
+ throw new MethodNotAllowedException();
+ } else {
parent::add();
- //$this->set('title_for_layout', 'Generated '.$this->viewVars['vv_authenticator']['Authenticator']['description']);
$this->set('title_for_layout', 'Generated Backup Codes');
if(!empty($this->request->params['named']['onFinish'])) {
@@ -71,7 +83,6 @@ public function generate() {
if(!empty($tokenInfo['otps'])) {
$this->set('vv_otps', (array)$tokenInfo['otps']);
- debug($vv_otps);
}
}
catch(Exception $e) {
@@ -83,7 +94,7 @@ public function generate() {
/**
* Callback before other controller methods are invoked or views are rendered.
*
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
*/
public function beforeFilter() {
@@ -100,7 +111,7 @@ public function beforeFilter() {
* This method is intended to be overridden by model-specific controllers.
* - postcondition: Session flash message updated (HTML) or HTTP status returned (REST)
*
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @param Array Current data
* @return boolean true if dependency checks succeed, false otherwise.
*/
@@ -131,7 +142,7 @@ function checkDeleteDependencies($curdata) {
* try{} block so that HistoryRecord->record() may be called without worrying
* about catching exceptions.
*
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @param String Controller action causing the change
* @param Array Data provided as part of the action (for add/edit)
* @param Array Previous data (for delete/edit)
@@ -172,7 +183,7 @@ public function generateHistory($action, $newdata, $olddata) {
* - precondition: Session.Auth holds data used for authz decisions
* - postcondition: $permissions set with calculated permissions
*
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @return Array Permissions
*/
@@ -195,4 +206,4 @@ function isAuthorized() {
$this->set('permissions', $p);
return($p[$this->action]);
}
-}
\ No newline at end of file
+}
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
index 8eb162970..f0993c7fb 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
@@ -74,5 +74,8 @@
'pl.privacyideaauthenticator.totp.step2' => 'Then, enter the current code from the Google Authenticator app to confirm',
'pl.privacyideaauthenticator.paper.intro' => 'Use the backup codes in order, one after the other. Mark off used values.',
'pl.privacyideaauthenticator.paper.caution' => 'Backup codes are a weak second factor. Please assure no one has access to these values. Store them in a safe location',
- 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.'
+ 'pl.privacyideaauthenticator.paper.warning' => 'Before you leave this page, please confirm that you have copied your backup codes. YOU WILL NOT SEE THEM AGAIN.',
+ 'pl.privacyideaauthenticator.paper.dialog' => 'Before you leave this page you must save your backup codes by copying or printing.',
+ 'pl.privacyideaauthenticator.paper.dialog.btn' => 'I understand',
+ 'pl.privacyideaauthenticator.paper.continue' => 'Once you have copied your backup codes, you must continue to the next step',
);
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
index ffbffff0d..074c72562 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdea.php
@@ -204,25 +204,23 @@ public function createToken($privacyIdeaAuthenticator, $coPersonId) {
case PrivacyIDEATokenTypeEnum::TOTP:
$token['confirmed'] = false;
$TotpToken = new TotpToken();
- $TotpToken->save($token);
-
+ if (!$TotpToken->save($token)) {
+ throw new RuntimeException(_txt('er.db.save-a', array('TotpToken')));
+ }
// We don't persist the QR Data, but we do need to return it for rendering
$token['qr_data'] = $jresponse->detail->googleurl->img;
break;
case PrivacyIDEATokenTypeEnum::Paper:
$PaperToken = new PaperToken();
- $PaperToken->save($token);
-
+ if (!$PaperToken->save($token)) {
+ throw new RuntimeException(_txt('er.db.save-a', array('PaperToken')));
+ }
// We don't persist the codes themselves but need to present them to the user for copying/printing
$token['otps'] = (array)$jresponse->detail->otps;
break;
}
- if(!$jresponse->result->status) {
- throw new RuntimeException($jresponse->result->error->message);
- }
-
return $token;
}
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
index 1819cdca3..b3dcaa220 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Model/PrivacyIdeaAuthenticator.php
@@ -147,7 +147,7 @@ public function current($id, $backendId, $coPersonId) {
$results = $this->TotpToken->find('all', $args);
if(empty($results)) {
- unset($args);
+ $args = array();
$args['conditions']['PaperToken.co_person_id'] = $coPersonId;
$args['conditions']['PaperToken.privacy_idea_authenticator_id'] = $backendId;
$args['contain'] = false;
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
index 06fa5d16f..5fbae8210 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
@@ -21,7 +21,7 @@
*
* @link http://www.internet2.edu/comanage COmanage Project
* @package registry-plugin
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-->
@@ -55,6 +55,9 @@
warning
+ request->params['named']['onFinish'])): ?>
+
+
-
-
-
+
+
+
- #
- OTP
-
-
-
- $otp): ?>
-
-
-
-
-
-
-
-
-
- print backup codes
+ #
+ OTP
+
+
+
+ $otp): ?>
+
+
+
+
+
+
+
+
+ request->params['named']['onFinish'])): ?>
+ Html->link(_txt('op.cont'),
+ urldecode($this->request->params['named']['onFinish']),
+ array('class' => 'btn btn-primary btn-lg'));
+ ?>
+ print backup codes
+
+ print backup codes
+
+
action == 'view'): ?>
-
+
+
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
index b6cc885c9..5cdd62824 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/index.ctp
@@ -21,7 +21,7 @@
*
* @link http://www.internet2.edu/comanage COmanage Project
* @package registry-plugin
- * @since COmanage Registry v4.3.0
+ * @since COmanage Registry v4.4.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
diff --git a/app/Config/bootstrap.php b/app/Config/bootstrap.php
index 81878787b..a3b093e52 100644
--- a/app/Config/bootstrap.php
+++ b/app/Config/bootstrap.php
@@ -128,12 +128,12 @@
*/
App::uses('CakeLog', 'Log');
CakeLog::config('debug', array(
- 'engine' => 'FileLog',
+ 'engine' => 'ConsoleLog',
'types' => array('notice', 'info', 'debug'),
'file' => 'debug',
));
CakeLog::config('error', array(
- 'engine' => 'FileLog',
+ 'engine' => 'ConsoleLog',
'types' => array('warning', 'error', 'critical', 'alert', 'emergency'),
'file' => 'error',
));
From f533476093b4f07cfb577c6fb6c4b183760a92bb Mon Sep 17 00:00:00 2001
From: Shayna Atkinson
Date: Tue, 31 Oct 2023 19:23:44 -0400
Subject: [PATCH 3/3] only allow one set up backup codes
---
.../Controller/PaperTokensController.php | 56 ++++++++++++-------
.../PrivacyIdeaAuthenticator/Lib/lang.php | 1 +
.../View/PaperTokens/generate.ctp | 7 ++-
.../View/PaperTokens/index.ctp | 2 +-
4 files changed, 44 insertions(+), 22 deletions(-)
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
index fafb3cb06..b4fc2edd5 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Controller/PaperTokensController.php
@@ -58,35 +58,51 @@ public function generate() {
if(!$this->request->is('get')) {
throw new MethodNotAllowedException();
} else {
- parent::add();
- $this->set('title_for_layout', 'Generated Backup Codes');
- if(!empty($this->request->params['named']['onFinish'])) {
- $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']);
- }
+ $args = array();
+ $args['conditions']['CoPerson.id'] = $this->viewVars['vv_co_person']['CoPerson']['id'];
+
+ $ppt = $this->PaperToken->find('first', $args);
+
+
+ if($ppt) {
+ $this->set('title_for_layout', 'Token already Exists');
+ $this->Flash->set(_txt('er.privacyideaauthenticator.singular'), array('key' => 'error'));
+ if(!empty($this->request->params['named']['onFinish'])) {
+ $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']);
+ }
+ } else {
+ parent::add();
- try {
- $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'],
+ $this->set('title_for_layout', 'Generated Backup Codes');
+
+ if(!empty($this->request->params['named']['onFinish'])) {
+ $this->set('vv_on_finish_url', $this->request->params['named']['onFinish']);
+ }
+
+ try {
+ $tokenInfo = $this->PrivacyIdea->createToken($this->viewVars['vv_authenticator']['PrivacyIdeaAuthenticator'],
$this->viewVars['vv_co_person']['CoPerson']['id']);
- $this->set('vv_token_info', $tokenInfo);
+ $this->set('vv_token_info', $tokenInfo);
- $newdata = array(
- 'PaperToken' => array(
- 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'],
- 'serial' => $tokenInfo['serial']
- )
- );
+ $newdata = array(
+ 'PaperToken' => array(
+ 'co_person_id' => $this->viewVars['vv_co_person']['CoPerson']['id'],
+ 'serial' => $tokenInfo['serial']
+ )
+ );
- $this->generateHistory('generate', $newdata, array());
+ $this->generateHistory('generate', $newdata, array());
- if(!empty($tokenInfo['otps'])) {
- $this->set('vv_otps', (array)$tokenInfo['otps']);
+ if(!empty($tokenInfo['otps'])) {
+ $this->set('vv_otps', (array)$tokenInfo['otps']);
+ }
+ }
+ catch(Exception $e) {
+ $this->Flash->set($e->getMessage(), array('key' => 'error'));
}
- }
- catch(Exception $e) {
- $this->Flash->set($e->getMessage(), array('key' => 'error'));
}
}
}
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
index f0993c7fb..f03096910 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/Lib/lang.php
@@ -58,6 +58,7 @@
// Error messages
'er.privacyideaauthenticator.code' => 'Invalid code, please try again',
'er.privacyideaauthenticator.identifier' => 'No Identifier of type "%1$s" found for CO Person',
+ 'er.privacyideaauthenticator.singular' => 'A token already exists; please delete that one before generating a new one',
// Plugin texts
'pl.privacyideaauthenticator.alt.google' => 'QR Code for Google Authenticator',
diff --git a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
index 5fbae8210..95f7b9ce3 100644
--- a/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
+++ b/app/AvailablePlugin/PrivacyIdeaAuthenticator/View/PaperTokens/generate.ctp
@@ -38,7 +38,6 @@
urldecode($this->request->params['named']['onFinish']),
array('class' => 'forwardbutton'));
}
- print $this->element("pageTitleAndButtons", $params);
// Add breadcrumbs
print $this->element("coCrumb", array('authenticator' => 'PrivacyIdea'));
@@ -112,6 +111,12 @@
+request->params['named']['onFinish'])): ?>
+ Html->link(_txt('op.cont'),
+ urldecode($this->request->params['named']['onFinish']),
+ array('class' => 'btn btn-primary btn-lg'));
+ ?>
action == 'view'): ?>