Repository navigation
Add explicit, secure identity linking for authenticated users - #77
Merged
Merged
Conversation
A provider package had no way to attach a sign-in method to an account other than inferring consent from the fact that somebody was logged in. That conflates two different decisions: resolving an account from a verified address is how a provider signs you in, and it is not good enough to attach a standing new credential, because a wrong sign-in ends with a session and a wrong link ends with a way back in the owner never chose. The mechanism keeps them apart structurally rather than by convention. It cannot authenticate anybody -- no access to a guard's login, every method returns a value object or nothing -- and it accepts no email address anywhere, so a matching address cannot become authorisation even by mistake. An attempt requires a credential confirmed within `auth.password_timeout`, which for a browser is Laravel's own `auth.password_confirmed_at` and for a token client is a password proved on the request. It then carries a single-use token, 256 bits stored only as its hash, bound to the user, their model class, the guard, the provider and the session or access token it began from; all of them are compared again on the way back. Spending is read-and-delete under an atomic lock where the store offers one. An identity owned by another account is refused. The current user is never switched and the identity is never transferred -- the only outcomes are allowed and an exception, whose reason is a closed enum so a web flow and an API can handle the same refusal differently without parsing prose.
The four cases the issue names, plus the ones that make them mean something: a callback landing on a different session, an intent finished against a different provider, an aged-out confirmation, and a token client proving its password. Two assert properties rather than behaviour. One checks that nothing is signed in after a link completes, which is the whole point of keeping linking separate from sign-in. The other walks the surface by reflection and asserts no parameter or field is named for an email address -- "do not treat a matching email as authorization" is a property of the shape, and a shape is worth asserting because it cannot drift the way a branch can. Proven red four ways: dropping the reauthentication requirement fails 3, reading the intent without spending it fails the replay case, unbinding the intent fails 3, and permitting an identity another user owns fails 1.
States what each step refuses and why the attempt is single-use and bound, so a provider package implementing a connect flow does not have to infer the contract from the signatures. Says plainly why this is separate from signing in: the two paths have different blast radii, and the separation is structural -- there is nowhere to pass an address and no way to authenticate.
…y-linking # Conflicts: # CHANGELOG.md # config/laranail/authkit.php # docs/configuration.md
Resolving the merge kept both sides of each conflict, which was right, but the conflict region in the config ended inside the `login` array rather than after it. Concatenating the two sides therefore nested `identity_linking` inside `login` and left the file one `]` short, so it did not parse at all. Close `login` before `identity_linking` opens. Both are top-level keys again, which the suite asserts by reading them through the booted container.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A provider package had no way to attach a sign-in method to an account except by inferring consent
from the fact that somebody was logged in. This adds the explicit mechanism:
IdentityLinkingInterface,bound to
Services\IdentityLinking.AuthKit owns whether a link is allowed. The provider package owns where links are stored and who
currently holds an identity, because it owns the table.
Why linking is not sign-in
Resolving an account from a verified address is how a provider signs you in. It is not good enough
to attach a standing new credential, because the two have different blast radii: a wrong sign-in
ends with a session, a wrong link ends with a way back in that the owner never chose.
The separation is structural, not a convention anyone has to remember:
object or nothing. A test asserts nobody is signed in after a link completes.
LinkIntent. "Do not treat amatching email as authorization" is a property of the shape, so a reflection test asserts the
shape rather than a branch — a branch can drift, a missing parameter cannot.
The four security requirements, and where each lands
auth.password_confirmed_atagainstauth.password_timeoutfor a session — exactly what Laravel'spassword.confirmmaintains — or a password proved on the request for a token client, checked through the guard's own providerassertLinkablereturns for a free identity, is idempotent for one this user already owns, and otherwise throws. The only outcomes are allowed and an exceptionRefusals carry a closed
IdentityLinkFailureenum rather than a message, so a web flow redirectingto password confirmation and an API returning 403 can handle the same refusal differently without
parsing prose.
Verification
laranail-pint --testProven red before being relied on, four separate ways:
Not in this change
authkit-social-loginstill resolves identities the old way, and adopting this mechanism there isauthkit-social-login#12.
Reviewing that package raised a separate concern in its existing resolution path. It is not
described here, because that package is public and the concern is unfixed; it has been reported to
the maintainer privately.
Closes #64.