reauth 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {reauth-0.2.0 → reauth-0.2.2}/PKG-INFO +2 -2
  2. {reauth-0.2.0 → reauth-0.2.2}/reauth/__init__.py +1 -1
  3. {reauth-0.2.0 → reauth-0.2.2}/reauth/authentication_session.py +4 -1
  4. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/backup_codes.py +4 -1
  5. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/base.py +13 -1
  6. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/email_otp.py +2 -1
  7. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/hotp.py +2 -1
  8. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/apple.py +3 -2
  9. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/base.py +2 -1
  10. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/github.py +2 -0
  11. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/google.py +3 -2
  12. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/oidc.py +13 -3
  13. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/totp.py +2 -1
  14. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_oidc.py +3 -4
  15. {reauth-0.2.0 → reauth-0.2.2}/tests/test_authentication_session.py +38 -0
  16. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/.gitkeep +0 -0
  17. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/authentication-terminology/SKILL.md +0 -0
  18. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/openid-connect-core/SKILL.md +0 -0
  19. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/openid-connect-core/references/openid-connect-core-1_0.txt +0 -0
  20. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/openid-connect-discovery/SKILL.md +0 -0
  21. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/openid-connect-discovery/references/openid-connect-discovery-1_0.txt +0 -0
  22. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/owasp-cheatsheets/SKILL.md +0 -0
  23. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc-spec/SKILL.md +0 -0
  24. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc-spec/scripts/create-rfc-skill.sh +0 -0
  25. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc4226/SKILL.md +0 -0
  26. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc4226/references/RFC4226.txt +0 -0
  27. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc6238/SKILL.md +0 -0
  28. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc6238/references/RFC6238.txt +0 -0
  29. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc6749/SKILL.md +0 -0
  30. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc6749/references/RFC6749.txt +0 -0
  31. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc7523/SKILL.md +0 -0
  32. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc7523/references/RFC7523.txt +0 -0
  33. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc7636/SKILL.md +0 -0
  34. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc7636/references/RFC7636.txt +0 -0
  35. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc8176/SKILL.md +0 -0
  36. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/rfc8176/references/RFC8176.txt +0 -0
  37. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/webauthn-2/SKILL.md +0 -0
  38. {reauth-0.2.0 → reauth-0.2.2}/.agents/skills/webauthn-2/references/webauthn-2.txt +0 -0
  39. {reauth-0.2.0 → reauth-0.2.2}/.editorconfig +0 -0
  40. {reauth-0.2.0 → reauth-0.2.2}/.github/workflows/build.yml +0 -0
  41. {reauth-0.2.0 → reauth-0.2.2}/.github/workflows/copilot-setup-steps.yml +0 -0
  42. {reauth-0.2.0 → reauth-0.2.2}/.github/workflows/documentation.yml +0 -0
  43. {reauth-0.2.0 → reauth-0.2.2}/.gitignore +0 -0
  44. {reauth-0.2.0 → reauth-0.2.2}/.zed/settings.json +0 -0
  45. {reauth-0.2.0 → reauth-0.2.2}/AGENTS.md +0 -0
  46. {reauth-0.2.0 → reauth-0.2.2}/LICENSE +0 -0
  47. {reauth-0.2.0 → reauth-0.2.2}/README.md +0 -0
  48. {reauth-0.2.0 → reauth-0.2.2}/docs/index.md +0 -0
  49. {reauth-0.2.0 → reauth-0.2.2}/docs/reference/reauth.md +0 -0
  50. {reauth-0.2.0 → reauth-0.2.2}/justfile +0 -0
  51. {reauth-0.2.0 → reauth-0.2.2}/mkdocs.yml +0 -0
  52. {reauth-0.2.0 → reauth-0.2.2}/pyproject.toml +0 -0
  53. {reauth-0.2.0 → reauth-0.2.2}/reauth/amr.py +0 -0
  54. {reauth-0.2.0 → reauth-0.2.2}/reauth/crypto.py +0 -0
  55. {reauth-0.2.0 → reauth-0.2.2}/reauth/exceptions.py +0 -0
  56. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/__init__.py +0 -0
  57. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/__init__.py +0 -0
  58. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/pkce.py +0 -0
  59. {reauth-0.2.0 → reauth-0.2.2}/reauth/factors/oauth2/state.py +0 -0
  60. {reauth-0.2.0 → reauth-0.2.2}/reauth/logging.py +0 -0
  61. {reauth-0.2.0 → reauth-0.2.2}/reauth/py.typed +0 -0
  62. {reauth-0.2.0 → reauth-0.2.2}/reauth/timestamp.py +0 -0
  63. {reauth-0.2.0 → reauth-0.2.2}/solo.yml +0 -0
  64. {reauth-0.2.0 → reauth-0.2.2}/tests/__init__.py +0 -0
  65. {reauth-0.2.0 → reauth-0.2.2}/tests/conftest.py +0 -0
  66. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/__init__.py +0 -0
  67. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/__init__.py +0 -0
  68. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/conftest.py +0 -0
  69. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_apple.py +0 -0
  70. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_base.py +0 -0
  71. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_github.py +0 -0
  72. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_pkce.py +0 -0
  73. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/oauth2/test_state.py +0 -0
  74. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/test_backup_codes.py +0 -0
  75. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/test_email_otp.py +0 -0
  76. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/test_hotp.py +0 -0
  77. {reauth-0.2.0 → reauth-0.2.2}/tests/factors/test_totp.py +0 -0
  78. {reauth-0.2.0 → reauth-0.2.2}/tests/test_crypto.py +0 -0
  79. {reauth-0.2.0 → reauth-0.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: reauth
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: The authentication toolkit for Python
5
5
  Project-URL: Documentation, https://frankie567.github.io/reauth/
6
6
  Project-URL: Source, https://github.com/frankie567/reauth
@@ -2,6 +2,6 @@
2
2
 
3
3
  from reauth.logging import configure_logger, get_logger
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.2"
6
6
 
7
7
  __all__ = ["__version__", "configure_logger", "get_logger"]
@@ -212,7 +212,8 @@ class AuthenticationSessionService(abc.ABC):
212
212
  authentication_session.identity_id = identity_id
213
213
  authentication_session.amr.append(factor.AMR)
214
214
  authentication_session.used_factors.append(factor.identifier)
215
- authentication_session.step += 1
215
+ previous_step = authentication_session.step
216
+ authentication_session.step += factor.advance_by
216
217
  await self.update(authentication_session)
217
218
 
218
219
  logger.info(
@@ -222,6 +223,8 @@ class AuthenticationSessionService(abc.ABC):
222
223
  "identity_id": identity_id,
223
224
  "factor_identifier": factor.identifier,
224
225
  "factor_amr": str(factor.AMR),
226
+ "previous_step": previous_step,
227
+ "advance_by": factor.advance_by,
225
228
  "step": authentication_session.step,
226
229
  },
227
230
  )
@@ -68,6 +68,7 @@ class BackupCodesFactor(FactorBase[BackupCodesEnrollment], abc.ABC):
68
68
  chars: str = DEFAULT_CHARS,
69
69
  identifier: str = "backup_codes",
70
70
  step: int = 1,
71
+ advance_by: int = 1,
71
72
  ) -> None:
72
73
  """
73
74
  Initialize the backup codes factor.
@@ -79,8 +80,10 @@ class BackupCodesFactor(FactorBase[BackupCodesEnrollment], abc.ABC):
79
80
  chars: Character set for code generation (default: excludes ambiguous chars).
80
81
  identifier: Unique identifier for the factor (default: "backup_codes").
81
82
  step: Authentication step at which this factor can be used (default: 1).
83
+ advance_by: Number of steps to advance after successful verification
84
+ (default: 1).
82
85
  """
83
- super().__init__(identifier=identifier, step=step)
86
+ super().__init__(identifier=identifier, step=step, advance_by=advance_by)
84
87
  self.hash_secret = hash_secret
85
88
  self.code_length = code_length
86
89
  self.code_count = code_count
@@ -6,6 +6,8 @@ from reauth.logging import get_logger
6
6
 
7
7
  logger = get_logger(__name__)
8
8
 
9
+ _ADVANCE_BY_VALUE_ERROR = "advance_by must be at least 1"
10
+
9
11
 
10
12
  class FactorEnrollment(typing.Protocol):
11
13
  """
@@ -22,7 +24,7 @@ class FactorBase[ENROLLMENT: FactorEnrollment](abc.ABC):
22
24
 
23
25
  AMR: typing.ClassVar[AuthenticationMethodReference]
24
26
 
25
- def __init__(self, *, identifier: str, step: int = 0) -> None:
27
+ def __init__(self, *, identifier: str, step: int = 0, advance_by: int = 1) -> None:
26
28
  """
27
29
  Initialize the factor base.
28
30
 
@@ -32,17 +34,27 @@ class FactorBase[ENROLLMENT: FactorEnrollment](abc.ABC):
32
34
  step: The authentication step at which this factor can be used.
33
35
  Factors are only available when the session's current step matches this value.
34
36
  Defaults to 0 (can be first factor).
37
+ advance_by: The number of steps to advance the authentication session after
38
+ this factor is successfully verified. Defaults to 1.
39
+
40
+ Raises:
41
+ ValueError: If advance_by is less than 1.
35
42
  """
43
+ if advance_by < 1:
44
+ raise ValueError(_ADVANCE_BY_VALUE_ERROR)
45
+
36
46
  logger.debug(
37
47
  "Factor initialized",
38
48
  extra={
39
49
  "identifier": identifier,
40
50
  "step": step,
51
+ "advance_by": advance_by,
41
52
  "amr": str(self.AMR),
42
53
  },
43
54
  )
44
55
  self.identifier = identifier
45
56
  self.step = step
57
+ self.advance_by = advance_by
46
58
 
47
59
  @abc.abstractmethod
48
60
  async def get_enrollment(self, identity_id: typing.Any) -> ENROLLMENT | None:
@@ -77,8 +77,9 @@ class EmailOTPFactor(FactorBase[EmailOTPEnrollment], abc.ABC):
77
77
  lifetime: datetime.timedelta = datetime.timedelta(minutes=10),
78
78
  identifier: str = "email_otp",
79
79
  step: int = 0,
80
+ advance_by: int = 1,
80
81
  ) -> None:
81
- super().__init__(identifier=identifier, step=step)
82
+ super().__init__(identifier=identifier, step=step, advance_by=advance_by)
82
83
  self.hash_secret = hash_secret
83
84
  self.code_length = code_length
84
85
  self.lifetime = lifetime
@@ -97,8 +97,9 @@ class HOTPFactor(FactorBase[HOTPEnrollment], abc.ABC):
97
97
  look_ahead: int = 5,
98
98
  identifier: str = "hotp",
99
99
  step: int = 1,
100
+ advance_by: int = 1,
100
101
  ) -> None:
101
- super().__init__(identifier=identifier, step=step)
102
+ super().__init__(identifier=identifier, step=step, advance_by=advance_by)
102
103
  self.code_length = code_length
103
104
  self.algorithm: HOTPAlgorithm = algorithm
104
105
  self.look_ahead = look_ahead
@@ -23,8 +23,6 @@ class AppleOAuth2Factor(OIDCFactorBase, abc.ABC):
23
23
  - Apple: https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
24
24
  """
25
25
 
26
- DISCOVERY_ENDPOINT = "https://appleid.apple.com/.well-known/openid-configuration"
27
-
28
26
  def __init__(
29
27
  self,
30
28
  *,
@@ -35,11 +33,14 @@ class AppleOAuth2Factor(OIDCFactorBase, abc.ABC):
35
33
  key_value: str,
36
34
  state_service: OAuth2StateService,
37
35
  step: int = 0,
36
+ advance_by: int = 1,
38
37
  ) -> None:
39
38
  super().__init__(
40
39
  identifier=identifier,
41
40
  step=step,
41
+ advance_by=advance_by,
42
42
  client_id=client_id,
43
+ discovery_endpoint="https://appleid.apple.com/.well-known/openid-configuration",
43
44
  state_service=state_service,
44
45
  )
45
46
  self.team_id = team_id
@@ -220,8 +220,9 @@ class OAuth2Factor[EXTRA](FactorBase[OAuth2Enrollment], abc.ABC):
220
220
  client_id: str,
221
221
  state_service: OAuth2StateService,
222
222
  step: int = 0,
223
+ advance_by: int = 1,
223
224
  ) -> None:
224
- super().__init__(identifier=identifier, step=step)
225
+ super().__init__(identifier=identifier, step=step, advance_by=advance_by)
225
226
  self.client_id = client_id
226
227
  self.state_service = state_service
227
228
 
@@ -88,10 +88,12 @@ class GitHubOAuth2Factor(OAuth2Factor[GitHubOAuth2Extra], abc.ABC):
88
88
  client_secret: str,
89
89
  state_service: OAuth2StateService,
90
90
  step: int = 0,
91
+ advance_by: int = 1,
91
92
  ) -> None:
92
93
  super().__init__(
93
94
  identifier=identifier,
94
95
  step=step,
96
+ advance_by=advance_by,
95
97
  client_id=client_id,
96
98
  state_service=state_service,
97
99
  )
@@ -12,8 +12,6 @@ class GoogleOAuth2Factor(OIDCFactor, abc.ABC):
12
12
  - Google: https://developers.google.com/identity/openid-connect/openid-connect
13
13
  """
14
14
 
15
- DISCOVERY_ENDPOINT = "https://accounts.google.com/.well-known/openid-configuration"
16
-
17
15
  def __init__(
18
16
  self,
19
17
  *,
@@ -22,11 +20,14 @@ class GoogleOAuth2Factor(OIDCFactor, abc.ABC):
22
20
  client_secret: str,
23
21
  state_service: OAuth2StateService,
24
22
  step: int = 0,
23
+ advance_by: int = 1,
25
24
  ) -> None:
26
25
  super().__init__(
27
26
  identifier=identifier,
28
27
  step=step,
28
+ advance_by=advance_by,
29
29
  client_id=client_id,
30
30
  client_secret=client_secret,
31
+ discovery_endpoint="https://accounts.google.com/.well-known/openid-configuration",
31
32
  state_service=state_service,
32
33
  )
@@ -308,22 +308,24 @@ class OIDCFactorBase(OAuth2Factor[OIDCExtraParams], abc.ABC):
308
308
  - OpenID Connect Core 1.0: https://openid.net/specs/openid-connect-core-1_0.html
309
309
  """
310
310
 
311
- DISCOVERY_ENDPOINT: typing.ClassVar[str]
312
-
313
311
  def __init__(
314
312
  self,
315
313
  *,
316
314
  identifier: str,
317
315
  client_id: str,
316
+ discovery_endpoint: str,
318
317
  state_service: OAuth2StateService,
319
318
  step: int = 0,
319
+ advance_by: int = 1,
320
320
  ) -> None:
321
321
  super().__init__(
322
322
  identifier=identifier,
323
323
  step=step,
324
+ advance_by=advance_by,
324
325
  client_id=client_id,
325
326
  state_service=state_service,
326
327
  )
328
+ self.discovery_endpoint = discovery_endpoint
327
329
  self._discovery_document: dict[str, typing.Any] | None = None
328
330
  self._jwks: jwt.PyJWKSet | None = None
329
331
  self._client = httpx.AsyncClient()
@@ -561,7 +563,7 @@ class OIDCFactorBase(OAuth2Factor[OIDCExtraParams], abc.ABC):
561
563
 
562
564
  client = self._get_client()
563
565
  try:
564
- response = await client.get(self.DISCOVERY_ENDPOINT)
566
+ response = await client.get(self.discovery_endpoint)
565
567
  response.raise_for_status()
566
568
  except httpx.HTTPError as e:
567
569
  raise DiscoveryDocumentException() from e
@@ -608,14 +610,18 @@ class OIDCFactor(OIDCFactorBase):
608
610
  identifier: str,
609
611
  client_id: str,
610
612
  client_secret: str,
613
+ discovery_endpoint: str,
611
614
  state_service: OAuth2StateService,
612
615
  step: int = 0,
616
+ advance_by: int = 1,
613
617
  ) -> None:
614
618
  super().__init__(
615
619
  identifier=identifier,
616
620
  client_id=client_id,
621
+ discovery_endpoint=discovery_endpoint,
617
622
  state_service=state_service,
618
623
  step=step,
624
+ advance_by=advance_by,
619
625
  )
620
626
  self._client_secret = client_secret
621
627
 
@@ -657,15 +663,19 @@ class PrivateKeyJWTOIDCFactor(OIDCFactorBase):
657
663
  client_id: str,
658
664
  jwks: jwt.PyJWKSet,
659
665
  kid: str,
666
+ discovery_endpoint: str,
660
667
  state_service: OAuth2StateService,
661
668
  assertion_lifetime: int = 60,
662
669
  step: int = 0,
670
+ advance_by: int = 1,
663
671
  ) -> None:
664
672
  super().__init__(
665
673
  identifier=identifier,
666
674
  client_id=client_id,
675
+ discovery_endpoint=discovery_endpoint,
667
676
  state_service=state_service,
668
677
  step=step,
678
+ advance_by=advance_by,
669
679
  )
670
680
  self._signing_jwks = jwks
671
681
  self._kid = kid
@@ -109,8 +109,9 @@ class TOTPFactor(FactorBase[TOTPEnrollment], abc.ABC):
109
109
  drift_tolerance: int = 1,
110
110
  identifier: str = "totp",
111
111
  step: int = 1,
112
+ advance_by: int = 1,
112
113
  ) -> None:
113
- super().__init__(identifier=identifier, step=step)
114
+ super().__init__(identifier=identifier, step=step, advance_by=advance_by)
114
115
  self.code_length = code_length
115
116
  self.algorithm: TOTPAlgorithm = algorithm
116
117
  self.time_step = time_step
@@ -136,8 +136,6 @@ def _create_token_response(
136
136
  class SQLAlchemyOIDCFactor(OIDCFactor):
137
137
  """Concrete implementation of OIDCFactor using SQLAlchemy for testing."""
138
138
 
139
- DISCOVERY_ENDPOINT = DISCOVERY_ENDPOINT
140
-
141
139
  def __init__(
142
140
  self,
143
141
  connection: AsyncConnection,
@@ -149,6 +147,7 @@ class SQLAlchemyOIDCFactor(OIDCFactor):
149
147
  identifier="oidc",
150
148
  client_id="test-client-id",
151
149
  client_secret="test-client-secret",
150
+ discovery_endpoint=DISCOVERY_ENDPOINT,
152
151
  state_service=state_service,
153
152
  )
154
153
 
@@ -654,8 +653,6 @@ class TestOIDCFactorExchangeCode:
654
653
 
655
654
 
656
655
  class _PrivateKeyJWTFactor(PrivateKeyJWTOIDCFactor):
657
- DISCOVERY_ENDPOINT = DISCOVERY_ENDPOINT
658
-
659
656
  async def insert(self, enrollment: OAuth2Enrollment) -> int:
660
657
  raise NotImplementedError()
661
658
 
@@ -686,6 +683,7 @@ class TestPrivateKeyJWTOIDCFactorGetRequestAuthentication:
686
683
  client_id="test-client-id",
687
684
  jwks=get_private_jwks_from_rsa_key(rsa_key, "sign-key-1"),
688
685
  kid="sign-key-1",
686
+ discovery_endpoint=DISCOVERY_ENDPOINT,
689
687
  state_service=oauth2_state_service,
690
688
  )
691
689
 
@@ -719,6 +717,7 @@ class TestPrivateKeyJWTOIDCFactorGetRequestAuthentication:
719
717
  client_id="test-client-id",
720
718
  jwks=get_private_jwks_from_rsa_key(rsa_key, "sign-key-1"),
721
719
  kid="unknown-key",
720
+ discovery_endpoint=DISCOVERY_ENDPOINT,
722
721
  state_service=oauth2_state_service,
723
722
  )
724
723
 
@@ -69,6 +69,20 @@ class DummyPasswordFactor(FactorBase[DummyPasswordFactorEnrollment]):
69
69
  return DummyPasswordFactorEnrollment(id=1, identity_id=identity_id)
70
70
 
71
71
 
72
+ class DummySSOFactor(FactorBase[DummyPasswordFactorEnrollment]):
73
+ AMR = AuthenticationMethodReference.OAUTH2
74
+
75
+ def __init__(self) -> None:
76
+ super().__init__(identifier="dummy_sso", step=0, advance_by=2)
77
+
78
+ async def get_enrollment(
79
+ self, identity_id: int
80
+ ) -> DummyPasswordFactorEnrollment | None:
81
+ if identity_id != 1:
82
+ return None
83
+ return DummyPasswordFactorEnrollment(id=1, identity_id=identity_id)
84
+
85
+
72
86
  @dataclasses.dataclass
73
87
  class DummyMFAFactorEnrollment:
74
88
  id: int | None
@@ -363,6 +377,30 @@ class TestAdvance:
363
377
  assert updated_session.id == session.id
364
378
  assert updated_session.amr == [AuthenticationMethodReference.PWD]
365
379
  assert updated_session.used_factors == ["dummy_password"]
380
+ assert updated_session.step == 1
381
+
382
+ async def test_factor_can_skip_next_step(
383
+ self,
384
+ sqlalchemy_connection: AsyncConnection,
385
+ mfa_factor: DummyMFAFactor,
386
+ ) -> None:
387
+ trusted_factor = DummySSOFactor()
388
+ authentication_session_service = SQLAlchemyAuthenticationSession(
389
+ connection=sqlalchemy_connection,
390
+ hash_secret="test_secret",
391
+ factors={trusted_factor, mfa_factor},
392
+ )
393
+ _token, session = await authentication_session_service.start()
394
+
395
+ updated_session = await authentication_session_service.advance(
396
+ session, 1, trusted_factor
397
+ )
398
+
399
+ assert updated_session.step == 2
400
+ assert (
401
+ await authentication_session_service.get_available_factors(updated_session)
402
+ == set()
403
+ )
366
404
 
367
405
  async def test_identity_one_factor_enrolled(
368
406
  self,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes