External Authentication Extension Tutorial
With External Authentication (or Inbound SSO), an external auth provider like Okta or Amazon Cognito will hold
the user credentials that can log into the Q2 system. However, the Q2 interface still has plenty of
places the user will interact with to alter these details, (we call these RequestTypes
), such as the Change Password interface.
Note
External Auth is not the same thing as (External MFA). For clarification, see our guide on the differences between External Auth and External MFA.
First, let’s ensure all the sql stored procedures for local testing are up to date:
$ q2 setup_db
Then, let’s create a new extension:
q2 create_extension
New Extension Name: ExternalAuth
What type of extension are you creating?
1) Online (default)
2) SSO (Third Party Integration)
3) Ardent (API)
4) Q2Console (Backoffice)
5) Central (Legacy Backoffice)
6) Adapter <--------
7) Audit Action
8) Custom Health Check
9) Message Bus
10) Caliper API Custom Endpoint
11) Base Extension
Please make a selection and press Return [1]: 6
Select adapter type to generate
1) Authentication Token
2) External Authentication <--------
3) FX Rate
4) Check Image
5) Account Details
6) Statement Image
7) Domestic Wire
8) International Wire
9) Realtime Payments
Please make a selection and press Return: 2
This should generate a lot of files (more than 30). Most of these are in a subdirectory called routes
, in which each file
corresponds to a RequestType
you can hook into. In all likelihood, you are not going to want to upgrade all of them, but
specific third party vendors may need a different set, so we make them all available. Later on, we’ll enable specific RequestTypes
to use your adapter rather than the default Q2 functionality. But first, we need to enable external_authentication at a system level.
For that, we can use the q2 sandbox external_auth
entrypoint:
$ q2 sandbox external_auth enable
Invalidating HQ
Enabled
Ths enables two System Properties:
Property Name |
Definition |
---|---|
EnableExternalEndUserAuthentication |
The master switch enabling the functionality |
ExternalEndUserAuthenticationRequiresSSOIdentifier |
Only logins in Q2_SSOUserLogon will use this system |
With those enabled, we need to register our test user to take part in enabled RequestTypes
.
Let’s add our user to the Q2_SSOUserLogon
table so it will get called during the workflow:
$ q2 db add_sso_user_logon retail0 myFakeGuid
Typically this is where we would decide which RequestTypes
we need to implement, then start building those handlers.
For the purposes of this tutorial, we’ll build two, Check Password for handling username and password on the Q2 side, and Logon User External
for logging in through another service.
Check Password
Route API Docs: Check Password
Take a look at the generated CheckPassword route (located in ExternalAuth/routes/check_password.py
):
from __future__ import annotations
from q2_sdk.hq.models.external_auth.check_password import Request, Response
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..extension import ExternalAuthHandler
async def route(self: ExternalAuthHandler, request: Request) -> Response:
"""
Successful responses return Response.get_success()
Failed responses return Response.get_failure()
"""
raise NotImplementedError
Clearly, this doesn’t do anything yet. However, that Request
and Response
refer specifically to what HQ will send to your
extension and what it will expect back. This true for each RequestType
, and more specific details can be found in
the External Auth Models Reference Page.
Before we start building the handling code, let’s enable the CheckPassword RequestType
for use by any users referenced in the
Q2_SSOUserLogon
table, to which we just added our test user:
$ q2 db set_external_auth_config CheckPassword --enabled
Note
To see which RequestTypes
your sandbox has enabled, you can use the q2 db get_external_auth_config
entrypoint
And register your adapter as the handler for all the RequestTypes:
$ q2 install
We’re ready! Let’s ensure everything is set up correctly. Start your server:
$ q2 run
And try to log in to Online Banking. From the front end, it should look like a login failure:

But the real test is whether your server is called. In your terminal logs you should see the following:
ExternalAuthType.CheckPassword is not implemented by your handler
Failed successfully! Let’s implement a success instead. Change your route code to this:
...
async def route(self: ExternalAuthHandler, request: Request) -> Response:
"""
Successful responses return Response.get_success()
Failed responses return Response.get_failure()
"""
return Response.get_success()
And when you try to log in to Online Banking again, you should be allowed in.
Logon User External
Route Api Docs:
When using an outside service for storing credentials, there are two calls made during the login flow. The first is called InitiateLogonUserExternal. This follows the OpenID Spec, and corresponds to the authorize endpoint (requiring a state, client_id, and redirect_uri)
The second is called LogonUserExternal (Without the Initiate prefix). This corresponds to the token endpoint. All that needs to be returned from this is the Foreign Key that ties together the Q2 User with the external system. Typically this is a guid, but could be something else. In the Q2 Database, it’s stored in the Q2_SSOUserLogon table in the SSOIdentifier field. We defined this as MyFakeGuid in the section above, but we’ll update it appropriately in just a moment.
Thankfully, the SDK makes this simple for the happy path. Assuming you are using a standard OAuth2 compliant service, you can define your configuration in the extension and write a very minimal amount of code. We’re going to use Q2Developer.com for this purpose today.
Before we start building the handling code, let’s enable the SSOAuth RequestType
for use by any users referenced in the
Q2_SSOUserLogon
table:
$ q2 db set_external_auth_config SSOAuth --enabled
Go ahead and start your server:
$ q2 run
And hit the special external sso login page, located at your sandbox’s normal online banking location, but rather than ending in /uux.aspx, it ends in /login?sso. For instance, if your typical login is https://stack.q2developer.com/sdk/finame/ardent/uux.aspx, for this you would hit https://stack.q2developer.com/sdk/finame/ardent/login?sso.
Unsurprisingly, that should fail:
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "An internal server error occurred"
}
But, in your terminal logs you should see:
ExternalAuthType.InitiateLogonUserExternal is not implemented by your handler
Nice. We’re being called. Just have to implement the Oauth2 logic flow. Your existing sso_auth route should look something like this:
from __future__ import annotations from q2_sdk.hq.models.external_auth.logon_user_external import Request, Response from q2_sdk.hq.models.external_auth.initiate_logon_user_external import Request as InitiateRequest, Response as InitiateResponse from typing import TYPE_CHECKING if TYPE_CHECKING: from ..extension import ExternalAuthHandler async def authorize_route(self: ExternalAuthHandler, request: InitiateRequest) -> InitiateResponse: """ Successful responses return InitiateResponse.get_success() Failed responses return InitiateResponse.get_failure() """ ## For Oauth2, uncomment this section and set extension's OAUTH2_CONFIG attribute # return await self.authorize_with_oauth2() raise NotImplementedError async def token_route(self: ExternalAuthHandler, request: Request) -> Response: """ Successful responses return Response.get_success() Failed responses return Response.get_failure() """ ## For Oauth2, uncomment this section and set extension's OAUTH2_CONFIG attribute # access_token = await self.get_oauth2_access_token(request) # uid = access_token["sub"] # return Response.get_success(uid) raise NotImplementedError
As instructed in the code comments, let’s remove the NotImplementedError lines and uncomment the other sections to transform it into this:
from __future__ import annotations from q2_sdk.hq.models.external_auth.logon_user_external import Request, Response from q2_sdk.hq.models.external_auth.initiate_logon_user_external import Request as InitiateRequest, Response as InitiateResponse from typing import TYPE_CHECKING if TYPE_CHECKING: from ..extension import ExternalAuthHandler async def authorize_route(self: ExternalAuthHandler, request: InitiateRequest) -> InitiateResponse: """ Successful responses return InitiateResponse.get_success() Failed responses return InitiateResponse.get_failure() """ return await self.authorize_with_oauth2() async def token_route(self: ExternalAuthHandler, request: Request) -> Response: """ Successful responses return Response.get_success() Failed responses return Response.get_failure() """ access_token = await self.get_oauth2_access_token(request) uid = access_token["sub"] return Response.get_success(uid)
And now we need to update the OAUTH2_CONFIG
variable in the extension.py
file.
By default that looks like this:
OAUTH2_CONFIG = Oauth2Config(
"client_id",
"client_secret",
"token_url",
"authorize_url",
"callback_url",
jwks_url="optional_jwks_url",
)
Let’s log in to https://q2developer.com and find the correct values for our use case. (Be sure to uncomment the Oauth2Config import line at the top of the file)
OAUTH2_CONFIG = Oauth2Config( "984a2702-2cdc-4aa6-ae2d-aaab3726c955", "72cc47c7795e3477e1e6cccf29d2aa45", "https://q2developer.com/oauth2/token", "https://q2developer.com/oauth2/authorize", "https://stack.q2developer.com/sdk/tigermfa/ardent/uux.aspx", jwks_url="https://q2developer.com/.well-known/jwks", # audience = "MyJWTAudienceValue" )
Q2Developer.com doesn’t specify an aud
claim, so we do not specify one in the config. If your IdP does then you can add the audience
parameter to the config
and the aud
claim will be checked to ensure it has the same value as the audience
parameter in the config.
And as simple as that we are now able to log in using an external source of truth.
After typing in your q2developer.com credentials, you should be redirected to the landing page of your sandbox stack. Nice work!