proposed federation.upstream-step

Sign in at an upstream provider through an AuthMethod

The person picks an upstream provider on the sign-in step. The method answers where the browser goes, the transaction it needs back and the binding (state) the provider carries back, which the engine keeps server-side, bound to the interaction and its flow cookie. The provider sends the browser back to the method's callback under /upstream/<type>; the host compares the callback's binding with the pending transaction's before it takes the transaction, once, and hands it to the callback, which checks the provider's answer. A result goes to verify(), and the engine checks the identity (its assurance level capped) and the attributes before the sign-in flow may use them. Whatever the method throws ends the step as a fault, never as a raw error.

What this view shows

the upstream step from the person's pick to the engine's completion, at the AuthMethod contract.

Open world: what is not drawn here is not ruled out. Only the guarantees below are binding.

Left out of this view:

The main story

When: An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag.

  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the callback hands a result to the engine, which passes it to verify()
  8. Turn the result into a VerifiedIdentity: the identity is checked and frozen, its assurance level lowered to the caller's ceiling: low unless the operator raised it for this provider, since an upstream sign-in ranks with a password
  9. Take the identity's attributes: every attribute's source is the method's type; the engine builds the list itself, so none is added

Result: Hand the sign-in flow a checked identity and its attributes.

The flow

the person picks a provider start() answers the redirect, the transaction or the binding fails a check; the browser goes nowhere the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser the provider posts its answer to the method's bounce route the provider sends the browser to the method's callback route, a GET under /upstream/<type> the bounce names a place other than the method's own GET callback routes, carries a query that is not a plain object of strings, or throws: nothing is redirected, and the mount hands the engine an AuthMethodFaultError through the host's completion, as for every other fault the browser follows the bounce to the method's GET callback no transaction is pending for this browser: a forged or forwarded callback, or a replay the binding is missing, empty, repeated or another start's: transaction_mismatch, and nothing is taken the binding is the pending transaction's the transaction is taken and handed to the callback the handler throws, or the method's context cannot be built: the transaction is spent, and the mount hands the engine an AuthMethodFaultError with the error as its cause, never a raw 500 the callback answers outside the closed set of outcomes the person declined at the provider the provider answered another error the provider's answer fails a check the provider cannot be reached through the egress client the callback hands a result to the engine, which passes it to verify() verify() rejects the result; its rejection is kept for the log verify() resolves something that is not an identity the engine accepts the identity is checked and frozen, its assurance level lowered to the caller's ceiling: low unless the operator raised it for this provider, since an upstream sign-in ranks with a password an attribute claims a source other than the method's type, or is malformed fails: extractAttributes() throws every attribute's source is the method's type; the engine builds the list itself, so none is added start The engine offers the step urn:qauth:ia:upstream:v1-rc.0 with the providers the realm lists, each with its display name, as an alternative of the first sign-in step; ADR-017's step table holds its params (#639). Offer the upstream step offered · server step start({ uiLocales }, context) answers where the browser goes (the provider's authorization endpoint with its parameters, the redirect_uri a URL under /upstream/<type>, a PKCE S256 challenge), the transaction the method needs back (a fresh state, a nonce, the PKCE verifier) and the binding: the fresh, random value the authorization request carries to the provider and the callback carries back, usually the state. Ask the method to start an upstream transaction starting · server step checkingStart Check what start() answered checkingStart · server step atProvider Wait while the person is at the provider atProvider · person The bounce route gets the form, no transaction and no context. The browser follows a 303 to the GET route on the same origin and brings the SameSite=Lax flow cookie with it. Send a form_post on to the method's own GET route bouncing · server step The host reads, without taking it, the pending transaction the flow cookie names, and compares the binding the callback carries (one value of the route's bindingParameter, state by default: from the query of a GET, the form of a POST) with the transaction's, in constant time. On any mismatch the method never runs, so it spends no code and calls no provider, and the pending sign-in stays pending: another site cannot cancel it. Compare the callback's binding with the pending transaction's, before taking it checkingBinding · server step The host takes it once: a second request for the same transaction finds none. It compares the binding of what it took once more (another start in the same browser may have replaced the transaction between the two reads), and the callback gets the query, the form and the transaction, and no header and no cookie. Take the pending transaction the flow cookie names takingTransaction · server step The method's own code, run only for a request whose binding matched. It redeems the code with its PKCE verifier through the egress client and checks the provider's answer; it may compare state with its transaction again, at no cost. The contract test kit holds every method to these rules: PKCE with S256, and a decline is declined, not a failure. The method's callback checks the provider's answer checkingCallback · server step verify() receives only what the method's own callback returned; the engine calls it, and nothing else does. It rejects anything else, a well-formed identity forged elsewhere among them (the kit checks that too). Turn the result into a VerifiedIdentity verifying · server step extractingAttributes Take the identity's attributes extractingAttributes · server step The sign-in flow (#639) looks the credential up by (realm, type, externalSub) and never matches an account by an attribute. Hand the sign-in flow a checked identity and its attributes ends here Nothing goes to the client, since the person has not refused the client's request. The person declined at the provider ends here failedTransactionMismatch Refuse a callback that does not belong to a pending transaction ends here failedProviderError The provider answered an error ends here failedInvalidResponse The provider's answer failed a check ends here failedUpstreamUnavailable The provider could not be reached ends here failedVerificationRefused verify() refused the result ends here An AuthMethodFaultError: the engine acts on none of the answer, and the sign-in flow shows its generic error page with a trace id. The error names the method's type for the operator's log. A fault the route mount caught reaches the engine as the outcome, which completeUpstreamStep throws like its own. The method broke the contract ends here
the person picks a provider start() answers fails: start() rejects, such as when the provider's discovery document cannot be read the redirect, the transaction or the binding fails a check; the browser goes nowhere the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser the provider posts its answer to the method's bounce route the provider sends the browser to the method's callback route, a GET under /upstream/<type> the person never comes back the bounce names a place other than the method's own GET callback routes, carries a query that is not a plain object of strings, or throws: nothing is redirected, and the mount hands the engine an AuthMethodFaultError through the host's completion, as for every other fault the browser follows the bounce to the method's GET callback no transaction is pending for this browser: a forged or forwarded callback, or a replay the binding is missing, empty, repeated or another start's: transaction_mismatch, and nothing is taken the binding is the pending transaction's the transaction is taken and handed to the callback the handler throws, or the method's context cannot be built: the transaction is spent, and the mount hands the engine an AuthMethodFaultError with the error as its cause, never a raw 500 the callback answers outside the closed set of outcomes the person declined at the provider the provider answered another error the provider's answer fails a check the provider cannot be reached through the egress client the callback hands a result to the engine, which passes it to verify() verify() rejects the result; its rejection is kept for the log verify() resolves something that is not an identity the engine accepts the identity is checked and frozen, its assurance level lowered to the caller's ceiling: low unless the operator raised it for this provider, since an upstream sign-in ranks with a password an attribute claims a source other than the method's type, or is malformed fails: extractAttributes() throws every attribute's source is the method's type; the engine builds the list itself, so none is added start The engine offers the step urn:qauth:ia:upstream:v1-rc.0 with the providers the realm lists, each with its display name, as an alternative of the first sign-in step; ADR-017's step table holds its params (#639). Offer the upstream step offered · server step start({ uiLocales }, context) answers where the browser goes (the provider's authorization endpoint with its parameters, the redirect_uri a URL under /upstream/<type>, a PKCE S256 challenge), the transaction the method needs back (a fresh state, a nonce, the PKCE verifier) and the binding: the fresh, random value the authorization request carries to the provider and the callback carries back, usually the state. Ask the method to start an upstream transaction starting · server step checkingStart Check what start() answered checkingStart · server step atProvider Wait while the person is at the provider atProvider · person The bounce route gets the form, no transaction and no context. The browser follows a 303 to the GET route on the same origin and brings the SameSite=Lax flow cookie with it. Send a form_post on to the method's own GET route bouncing · server step The host reads, without taking it, the pending transaction the flow cookie names, and compares the binding the callback carries (one value of the route's bindingParameter, state by default: from the query of a GET, the form of a POST) with the transaction's, in constant time. On any mismatch the method never runs, so it spends no code and calls no provider, and the pending sign-in stays pending: another site cannot cancel it. Compare the callback's binding with the pending transaction's, before taking it checkingBinding · server step The host takes it once: a second request for the same transaction finds none. It compares the binding of what it took once more (another start in the same browser may have replaced the transaction between the two reads), and the callback gets the query, the form and the transaction, and no header and no cookie. Take the pending transaction the flow cookie names takingTransaction · server step The method's own code, run only for a request whose binding matched. It redeems the code with its PKCE verifier through the egress client and checks the provider's answer; it may compare state with its transaction again, at no cost. The contract test kit holds every method to these rules: PKCE with S256, and a decline is declined, not a failure. The method's callback checks the provider's answer checkingCallback · server step verify() receives only what the method's own callback returned; the engine calls it, and nothing else does. It rejects anything else, a well-formed identity forged elsewhere among them (the kit checks that too). Turn the result into a VerifiedIdentity verifying · server step extractingAttributes Take the identity's attributes extractingAttributes · server step The sign-in flow (#639) looks the credential up by (realm, type, externalSub) and never matches an account by an attribute. Hand the sign-in flow a checked identity and its attributes ends here Nothing goes to the client, since the person has not refused the client's request. The person declined at the provider ends here failedTransactionMismatch Refuse a callback that does not belong to a pending transaction ends here failedProviderError The provider answered an error ends here failedInvalidResponse The provider's answer failed a check ends here failedUpstreamUnavailable The provider could not be reached ends here failedVerificationRefused verify() refused the result ends here start()'s own rejection, unchanged; the sign-in flow answers it like an unreachable provider. The method could not start ends here An AuthMethodFaultError: the engine acts on none of the answer, and the sign-in flow shows its generic error page with a trace id. The error names the method's type for the operator's log. A fault the route mount caught reaches the engine as the outcome, which completeUpstreamStep throws like its own. The method broke the contract ends here The transaction ends with its interaction. The person did not come back user leaves the page

blue line: the main storyred dashed line: a failure path (shown with “Show failure paths”)dotted line: decided by a rule, nobody actsa person does it (editor, review, approval)dashed grey box: the server decides, nothing is shown

What each step answers

StepThe outside world sees
Hand the sign-in flow a checked identity and its attributes
identityVerified
body: {"kind":"identity","identity":"{ externalSub, assuranceLevel, rawClaims }, checked and frozen","attributes":"the method's attributes, each with source equal to its type"}
The person declined at the provider
declined
body: {"kind":"declined"}
Refuse a callback that does not belong to a pending transaction
failedTransactionMismatch
body: {"kind":"failed","reason":"transaction_mismatch"}
The provider answered an error
failedProviderError
body: {"kind":"failed","reason":"provider_error"}
The provider's answer failed a check
failedInvalidResponse
body: {"kind":"failed","reason":"invalid_response"}
The provider could not be reached
failedUpstreamUnavailable
body: {"kind":"failed","reason":"upstream_unavailable"}
verify() refused the result
failedVerificationRefused
body: {"kind":"failed","reason":"verification_refused"}
The method broke the contract
methodFault
method_fault

How it decides what happens next

Check what start() answered

#IfThen
1the redirect, the transaction or the binding fails a check; the browser goes nowhereThe method broke the contract
2the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browserWait while the person is at the provider

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1startInvalidmethodFault2880 of 5760
2otherwiseatProvider2880 of 5760
startInvalid
startAnswer == 'invalid'

Send a form_post on to the method's own GET route

#IfThen
1the bounce names a place other than the method's own GET callback routes, carries a query that is not a plain object of strings, or throws: nothing is redirected, and the mount hands the engine an AuthMethodFaultError through the host's completion, as for every other faultThe method broke the contract
2the browser follows the bounce to the method's GET callbackCompare the callback's binding with the pending transaction's, before taking it

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1bouncesElsewheremethodFault720 of 1440
2otherwisecheckingBinding720 of 1440
bouncesElsewhere
bounceTarget == 'elsewhere'

Compare the callback's binding with the pending transaction's, before taking it

#IfThen
1no transaction is pending for this browser: a forged or forwarded callback, or a replayRefuse a callback that does not belong to a pending transaction
2the binding is missing, empty, repeated or another start's: transaction_mismatch, and nothing is takenRefuse a callback that does not belong to a pending transaction
3the binding is the pending transaction'sTake the pending transaction the flow cookie names

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1noTransactionfailedTransactionMismatch1440 of 2160
2bindingDiffersfailedTransactionMismatch360 of 2160
3otherwisetakingTransaction360 of 2160
noTransaction
arrival != 'sameBrowser'
bindingDiffers
bindingCheck == 'differs'

Take the pending transaction the flow cookie names

#IfThen
1the transaction is taken and handed to the callbackThe method's callback checks the provider's answer

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1otherwisecheckingCallback360 of 360

The method's callback checks the provider's answer

#IfThen
1the handler throws, or the method's context cannot be built: the transaction is spent, and the mount hands the engine an AuthMethodFaultError with the error as its cause, never a raw 500The method broke the contract
2the callback answers outside the closed set of outcomesThe method broke the contract
3the person declined at the providerThe person declined at the provider
4the provider answered another errorThe provider answered an error
5the provider's answer fails a checkThe provider's answer failed a check
6the provider cannot be reached through the egress clientThe provider could not be reached
7the callback hands a result to the engine, which passes it to verify()Turn the result into a VerifiedIdentity

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1callbackFaultsmethodFault180 of 360
2outcomeInvalidmethodFault90 of 360
3personDeclineddeclined18 of 360
4providerErrorfailedProviderError18 of 360
5answerInvalidfailedInvalidResponse18 of 360
6providerUnreachablefailedUpstreamUnavailable18 of 360
7otherwiseverifying18 of 360
callbackFaults
callbackRun == 'faults'
personDeclined
providerAnswer == 'declined'
providerError
providerAnswer == 'error'
answerInvalid
providerAnswer == 'invalid'
providerUnreachable
providerAnswer == 'unreachable'
outcomeInvalid
outcomeShape == 'invalid'

Turn the result into a VerifiedIdentity

#IfThen
1verify() rejects the result; its rejection is kept for the logverify() refused the result
2verify() resolves something that is not an identity the engine acceptsThe method broke the contract
3the identity is checked and frozen, its assurance level lowered to the caller's ceiling: low unless the operator raised it for this provider, since an upstream sign-in ranks with a passwordTake the identity's attributes

The first matching rule wins.

Exact conditions
#ConditionGoes toSituations
1verifyRejectsfailedVerificationRefused6 of 18
2identityMalformedmethodFault6 of 18
3otherwiseextractingAttributes6 of 18
identityMalformed
verifyAnswer == 'malformed'
verifyRejects
verifyAnswer == 'rejects'

Every way it can end

It ends withWhenWhat happens
Hand the sign-in flow a checked identity and its attributes
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the callback hands a result to the engine, which passes it to verify()
  8. Turn the result into a VerifiedIdentity: the identity is checked and frozen, its assurance level lowered to the caller's ceiling: low unless the operator raised it for this provider, since an upstream sign-in ranks with a password
  9. Take the identity's attributes: every attribute's source is the method's type; the engine builds the list itself, so none is added
The person declined at the provider
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person cancelled or declined at the provider (error=access_denied), the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the person declined at the provider
Refuse a callback that does not belong to a pending transaction
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, it carries another one, or none, an empty one or the parameter repeated: a response meant for another start, a forged one, or a cross-site request that would cancel the pending sign-in, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is missing, empty, repeated or another start's: transaction_mismatch, and nothing is taken
The provider answered an error
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the provider answered another error, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the provider answered another error
The provider's answer failed a check
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the provider's answer fails a check: the code exchange, the ID token's signature, iss, aud or nonce, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the provider's answer fails a check
The provider could not be reached
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the provider cannot be reached in time, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the provider cannot be reached through the egress client
verify() refused the result
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, it rejects the result, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the provider sends the browser to the method's callback route, a GET under /upstream/<type>
  5. Compare the callback's binding with the pending transaction's, before taking it: the binding is the pending transaction's
  6. Take the pending transaction the flow cookie names: the transaction is taken and handed to the callback
  7. The method's callback checks the provider's answer: the callback hands a result to the engine, which passes it to verify()
  8. Turn the result into a VerifiedIdentity: verify() rejects the result; its rejection is kept for the log
The method could not start
ends here
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: it fails, because start() rejects, such as when the provider's discovery document cannot be read
The method broke the contract
ends here
Anything else: an http redirect, credentials, a fragment, a transaction that is not JSON data or too big, a binding that is missing, short or outside base64url, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the redirect, the transaction or the binding fails a check; the browser goes nowhere
The person did not come back
user leaves the page
An https redirect with no credentials and no fragment, a transaction of JSON data within 4096 bytes, and a binding of 22 to 512 base64url characters, a GET to the callback, the answer in the query, the bounce names one of the method's own GET callback routes, with a plain object of strings as its query, the browser that started brings its flow cookie, and the transaction is still pending, the callback carries the pending transaction's binding, once, the context is built and the handler answers, the person signed in, and the answer passes every check of the method, the callback answers result, declined, or failed with a reason of the closed set, verify() resolves an externalSub of 1 to 255 characters of well-formed text with no control character, an assurance level and raw claims, every attribute's source is the method's type, with a key, a string value and a verified flag
  1. Offer the upstream step: the person picks a provider
  2. Ask the method to start an upstream transaction: start() answers
  3. Check what start() answered: the engine keeps a frozen copy of the transaction, with its binding, server-side, bound to the interaction and its flow cookie, and sends the browser to the redirect; the transaction never reaches the browser
  4. Wait while the person is at the provider: the person never comes back

Each row is also a generated test: set the starting situation, send the events, expect the ending.

What is guaranteed

These hold in all 38907 situations the model can reach, from 5760 different starting situations.

Exact rules
IdNever
identity-needs-the-pending-transactionstate == 'identityVerified' and (not transactionTaken or noTransaction)
identity-needs-the-bindingstate == 'identityVerified' and bindingDiffers
a-foreign-callback-takes-nothingtransactionTaken and (noTransaction or bindingDiffers)
verify-only-sees-callback-resultsverifyCalled and (noTransaction or bindingDiffers or callbackFaults or outcomeInvalid)
a-fault-never-escapes-the-engineverifyCalled and callbackFaults
no-foreign-attribute-sourcestate == 'identityVerified' and foreignSource
a-broken-start-sends-the-browser-nowheretransactionKept and startInvalid
a-decline-is-not-a-failurestate == 'declined' and not personDeclined
a-bounce-stays-on-the-methodstate == 'checkingBinding' and returnMode == 'formPost' and bouncesElsewhere

Reference

All transitions (27)
FromEventGuardToEffectsTaken
offeredPICKstartingyes
startingDONEcheckingStartyes
startingFAIL.rejectedstartFailedyes
checkingStartrule 1startInvalidmethodFaultyes
checkingStartrule 2atProviderkeepTransactionyes
atProviderBROWSER_RETURNSreturnMode == 'formPost'bouncingyes
atProviderBROWSER_RETURNScheckingBindingyes
atProviderABANDONabandonedyes
bouncingrule 1bouncesElsewheremethodFaultyes
bouncingrule 2checkingBindingyes
checkingBindingrule 1noTransactionfailedTransactionMismatchyes
checkingBindingrule 2bindingDiffersfailedTransactionMismatchyes
checkingBindingrule 3takingTransactionyes
takingTransactionrule 1checkingCallbacktakeTransactionyes
checkingCallbackrule 1callbackFaultsmethodFaultyes
checkingCallbackrule 2outcomeInvalidmethodFaultyes
checkingCallbackrule 3personDeclineddeclinedyes
checkingCallbackrule 4providerErrorfailedProviderErroryes
checkingCallbackrule 5answerInvalidfailedInvalidResponseyes
checkingCallbackrule 6providerUnreachablefailedUpstreamUnavailableyes
checkingCallbackrule 7verifyingcallVerifyyes
verifyingrule 1verifyRejectsfailedVerificationRefusedyes
verifyingrule 2identityMalformedmethodFaultyes
verifyingrule 3extractingAttributesyes
extractingAttributesDONEforeignSourcemethodFaultyes
extractingAttributesDONEidentityVerifiedyes
extractingAttributesFAIL.threwmethodFaultyes