Skip to content

Commit

Permalink
Support self-vetting of self-asserted tokens
Browse files Browse the repository at this point in the history
When an Identity is in possession of a vetted self-asserted token. It
should be allowed to self-vet successive tokens using that SAT. But only
when all tokens are of the SAT type. Once an Identity is in possession
of an idenity vetted token (vetted on-premise), all following self
vetting tokens must be done with the identity vetted token.

For details see: https://www.pivotaltracker.com/story/show/184292087
And: OpenConext/Stepup-SelfService#284
  • Loading branch information
MKodde committed Feb 16, 2023
1 parent a01b4fe commit 366e223
Show file tree
Hide file tree
Showing 19 changed files with 704 additions and 31 deletions.
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config/legacy/bundles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ surfnet_stepup:
loa1: "%stepup_loa_loa1%"
loa2: "%stepup_loa_loa2%"
loa3: "%stepup_loa_loa3%"
loa-self-asserted: "%stepup_loa_self_asserted%"
sms:
enabled: false

Expand Down
1 change: 1 addition & 0 deletions config/legacy/parameters.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ parameters:
stepup_loa_loa1: https://gateway.tld/authentication/loa1
stepup_loa_loa2: https://gateway.tld/authentication/loa2
stepup_loa_loa3: https://gateway.tld/authentication/loa3
stepup_loa_self_asserted: 'http://stepup.example.com/assurance/loa-self-asserted'

self_service_url: https://selfservice.tld

Expand Down
36 changes: 36 additions & 0 deletions docs/MiddlewareAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,42 @@ Example of possible error messages. These may differ in the real world, but give
```


### Allowed to self-vet using an existing self-asserted token?

Is the Identity allowed to self-vet a token using a self-asserted token (SAT). This is only allowed when the only token(s)
possessed by the identity are of the self-asserted token vetting type. If another token type is registered, the
middleware will nudge the user to use that token. As using a SAT token will result in a token with a lowered LoA. As the
identity of the user is not verified by a RA(A).

- URL: `http://middleware.tld/authorization/may-self-vet-using-self-asserted-token/{identityId}`
- Method: GET
- Request parameters:
- identityId: (required) UUIDv4 of the identity to assert the authorization for

### Response
`200 OK`
```json
{
"code": 200
}
```

`403 Forbidden`

Example of possible error messages. These may differ in the real world, but give a grasp on what they should look like.

```json
{
"code": 403,
"errors": [
"Not permitted: institution does not allow self-asserted tokens.",
"Not permitted: no recovery method found.",
"Not permitted: no vetted tokens may be in possession."
]
}
```


## Command API

For documentation of the commands that can be handled with the Command API. Please consult [this document](MiddlewareAPICommands.md).
Expand Down
46 changes: 38 additions & 8 deletions docs/postman/3.json
Original file line number Diff line number Diff line change
Expand Up @@ -899,10 +899,6 @@
{
"key": "orderDirection",
"value": "DESC"
},
{
"key": "XDEBUG_SESSION_START",
"value": "PHPSTORM"
}
]
},
Expand All @@ -918,10 +914,6 @@
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Basic cmE6YmFy"
}
],
"url": {
Expand Down Expand Up @@ -978,6 +970,44 @@
},
"response": []
},
{
"name": "/authorization/may-self-vet-using-self-asserted-token",
"request": {
"method": "GET",
"header": [
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "http://middleware.stepup.example.com/authorization/may-self-vet-using-self-asserted-token/cc51da47-8edf-4ebc-8192-7a26e348d193?XDEBUG_SESSION_START=PHPSTORM",
"protocol": "http",
"host": [
"middleware",
"stepup",
"example",
"com"
],
"path": [
"authorization",
"may-self-vet-using-self-asserted-token",
"cc51da47-8edf-4ebc-8192-7a26e348d193"
],
"query": [
{
"key": "XDEBUG_SESSION_START",
"value": "PHPSTORM"
}
]
}
},
"response": []
},
{
"name": "/authorization/may-register-recovery-tokens",
"request": {
Expand Down
8 changes: 7 additions & 1 deletion src/Surfnet/Stepup/Identity/Api/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,15 @@ public function vetSecondFactor(
);

/**
* Self vetting, is when the user uses it's own token to vet another.
* Self vetting, is when the user uses its own token to vet another.
*
* Here the new token should have a lower or equal LoA to that of the one in possession of the identity
*
* Alternatively, the selfVetSecondFactor allows for vetting of self-asserted tokens. In that case, a
* token that was activated using a self-asserted vetting method, is used to author the possession of
* a new verified token. Note that this newly self-vetted token will have a diminished LoA. This because
* the self-asserted token used to author itself has a deminished LoA level due to the lack of a vetted
* identity.
*/
public function selfVetSecondFactor(
Loa $authoringSecondFactorLoa,
Expand Down
10 changes: 10 additions & 0 deletions src/Surfnet/Stepup/Identity/Entity/RecoveryTokenCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Surfnet\Stepup\Identity\Value\RecoveryTokenId;
use Surfnet\Stepup\Identity\Value\RecoveryTokenType;
use function array_key_exists;
use function array_key_first;
use function array_shift;

final class RecoveryTokenCollection
{
Expand Down Expand Up @@ -62,4 +64,12 @@ public function remove(RecoveryTokenId $recoveryTokenId)
{
unset($this->recoveryTokens[(string)$recoveryTokenId]);
}

public function first(): RecoveryToken
{
if ($this->count() === 0) {
throw new DomainException('Unable to get the first recovery token from an empty collection');
}
return current($this->recoveryTokens);
}
}
3 changes: 2 additions & 1 deletion src/Surfnet/Stepup/Identity/Entity/VerifiedSecondFactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Surfnet\Stepup\Identity\Value\VettingType;
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
use Surfnet\StepupBundle\Value\SecondFactorType;
use Surfnet\StepupBundle\Value\VettingType as StepupVettingType;

/**
* A second factor whose possession has been proven by the registrant and the registrant's e-mail address has been
Expand Down Expand Up @@ -219,7 +220,7 @@ public function asVetted(VettingType $vettingType)

public function getLoaLevel(SecondFactorTypeService $secondFactorTypeService): int
{
return $secondFactorTypeService->getLevel($this->type);
return $secondFactorTypeService->getLevel($this->type, new StepupVettingType(VettingType::TYPE_UNKNOWN));
}

protected function applyIdentityForgottenEvent(IdentityForgottenEvent $event)
Expand Down
52 changes: 51 additions & 1 deletion src/Surfnet/Stepup/Identity/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,15 @@
use Surfnet\Stepup\Identity\Value\StepupProvider;
use Surfnet\Stepup\Identity\Value\U2fKeyHandle;
use Surfnet\Stepup\Identity\Value\UnknownVettingType;
use Surfnet\Stepup\Identity\Value\VettingType;
use Surfnet\Stepup\Identity\Value\YubikeyPublicId;
use Surfnet\Stepup\Token\TokenGenerator;
use Surfnet\StepupBundle\Security\OtpGenerator;
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
use Surfnet\StepupBundle\Value\Loa;
use Surfnet\StepupBundle\Value\SecondFactorType;
use Surfnet\StepupMiddleware\ApiBundle\Identity\Entity\RecoveryToken;
use function sprintf;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Expand Down Expand Up @@ -596,6 +598,27 @@ public function registerSelfAssertedSecondFactor(
$registeringSecondFactor->vet(true, new SelfAssertedRegistrationVettingType($recoveryToken->getTokenId()));
}

/**
* Two self-vet scenarios are dealt with
*
* 1. A regular self-vet action. Where an on premise token is used to vet another token
* from the comfort of the identity's SelfService application. In other words, self vetting
* allows the identity to activate a second/third/.. token without visiting the service desk
*
* 2. A variation on 1: but here a self-asserted token is used to activate the verified token.
* This new token will inherit the LoA of the self-asserted token. Effectively giving it a
* LoA 1.5 level.
*
* The code below uses the following terminology
*
* RegisteringSecondFactor: This is the verified second factor that is to be activated
* using the self-vet vetting type
* AuthoringSecondFactor: The vetted token, used to activate (vet) the RegisteringSecondFactor
* IsSelfVetUsingSAT: Is self-vetting using a self-asserted token allowed for this
* self-vet scenario? All existing vetted tokens must be of the
* self-asserted vetting type.
*
*/
public function selfVetSecondFactor(
Loa $authoringSecondFactorLoa,
string $registrationCode,
Expand Down Expand Up @@ -629,12 +652,23 @@ public function selfVetSecondFactor(
$registeringSecondFactor->getLoaLevel($secondFactorTypeService)
);

if (!$selfVettingIsAllowed) {
// Was the authorizing token a self-asserted token (does it have LoA 1.5?)
$isSelfVetUsingSAT = $authoringSecondFactorLoa->getLevel() === Loa::LOA_SELF_VETTED;

if (!$selfVettingIsAllowed && !$isSelfVetUsingSAT) {
throw new DomainException(
"The second factor to be vetted has a higher LoA then the Token used for proving possession"
);
}

if ($isSelfVetUsingSAT) {
// Assert that all previously vetted tokens are SAT tokens. If this is not the case, do not allow
// self vetting using a SAT.
$this->assertAllVettedTokensAreSelfAsserted();
$recoveryToken = $this->recoveryTokens->first();
$registeringSecondFactor->vet(true, new SelfAssertedRegistrationVettingType($recoveryToken->getTokenId()));
return;
}
$registeringSecondFactor->vet(true, new SelfVetVettingType($authoringSecondFactorLoa));
}

Expand Down Expand Up @@ -1488,4 +1522,20 @@ private function assertSelfAssertedTokenRegistrationAllowed()
throw new DomainException("A recovery token is required to perform a self-asserted token registration");
}
}

/**
* Verify that every vetted second factor is self-asserted
*/
private function assertAllVettedTokensAreSelfAsserted()
{
/** @var VettedSecondFactor $vettedToken */
foreach ($this->vettedSecondFactors as $vettedSecondFactor) {
if ($vettedSecondFactor->vettingType()->type() !== VettingType::TYPE_SELF_ASSERTED_REGISTRATION) {
throw new DomainException(
'Not all tokens are self-asserted, it is not allowed to self-vet using the self-asserted token'
);
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

use Surfnet\Stepup\Configuration\Value\Institution;
use Surfnet\Stepup\Identity\Value\IdentityId;
use Surfnet\Stepup\Identity\Value\VettingType;
use Surfnet\StepupMiddleware\ApiBundle\Authorization\Value\AuthorizationDecision;
use Surfnet\StepupMiddleware\ApiBundle\Configuration\Service\InstitutionConfigurationOptionsService;
use Surfnet\StepupMiddleware\ApiBundle\Identity\Query\VettedSecondFactorQuery;
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\IdentityService;
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\RecoveryTokenService;
use Surfnet\StepupMiddleware\ApiBundle\Identity\Service\SecondFactorService;
Expand Down Expand Up @@ -66,6 +68,15 @@ public function __construct(
$this->recoveryTokenService = $recoveryTokenService;
}

/**
* Is an identity is allowed to register a self asserted token.
*
* An identity can register a SAT when:
* - The institution of the identity allows SAT
* - Has not yet registered a SAT
* - Has not possessed a non SAT token previously.
* - It did not lose both the recovery token and self-asserted token
*/
public function assertRegistrationOfSelfAssertedTokensIsAllowed(IdentityId $identityId): AuthorizationDecision
{
$identity = $this->identityService->find((string)$identityId);
Expand Down Expand Up @@ -110,6 +121,51 @@ public function assertRegistrationOfSelfAssertedTokensIsAllowed(IdentityId $iden
return $this->allow();
}

/**
* Is an identity allowed to self vet using a self-asserted token?
*
* One is allowed to do so when:
* - SAT is allowed for the institution of the identity
* - All of the tokens of the identity are vetted using the SAT vetting type
*/
public function assertSelfVetUsingSelfAssertedTokenIsAllowed(IdentityId $identityId): AuthorizationDecision
{
$identity = $this->identityService->find((string)$identityId);
if (!$identity) {
return $this->deny('Identity not found');
}

$institution = new Institution((string)$identity->institution);
$institutionConfiguration = $this->institutionConfigurationService
->findInstitutionConfigurationOptionsFor($institution);
if (!$institutionConfiguration) {
return $this->deny('Institution configuration could not be found, unable to ascertain if self-asserted tokens feature is enabled');
}

if (!$institutionConfiguration->selfAssertedTokensOption->isEnabled()) {
return $this->deny(sprintf('Institution "%s", does not allow self-asserted tokens', (string) $identity->institution));
}

$query = new VettedSecondFactorQuery();
$query->identityId = $identityId;
$query->pageNumber = 1;
$tokens = $this->secondFactorService->searchVettedSecondFactors($query);
foreach ($tokens->getIterator() as $vettedToken) {
if ($vettedToken->vettingType !== VettingType::TYPE_SELF_ASSERTED_REGISTRATION) {
return $this->deny('Self-vetting using SAT is only allowed when only SAT tokens are in possession');
}
}

return $this->allow();
}

/**
* Is an identity allowed to register recovery tokens?
*
* One is allowed to do so when:
* - SAT is allowed for the institution of the identity
* - Identity must possess a SAT (or did so at one point)
*/
public function assertRecoveryTokensAreAllowed(IdentityId $identityId): AuthorizationDecision
{
$identity = $this->identityService->find((string)$identityId);
Expand Down
Loading

0 comments on commit 366e223

Please sign in to comment.