Hiya 
AshAuthentication (hereafter AA) does a lot of work to make it as drop-in as possible. Supporting a high level of customisation means that the code can sometimes look a big hairy because it has to check the strategy struct for everything that it can do. However, let’s dig into it a bit.
Strategies
When you add
authentication do
auth0 do
# ...
end
end
to your resource, it generates a strategy struct that looks something like this:
iex> Example.User |> AshAuthentication.Info.strategy!(:auth0)
%AshAuthentication.Strategy.OAuth2{
assent_strategy: Assent.Strategy.Auth0,
auth_method: :client_secret_post,
authorization_params: [scope: "openid profile email"],
authorize_url: "/authorize",
client_authentication_method: nil,
client_id: {AshAuthentication.SecretFunction,
[fun: &Example.User.client_id_0_generated_1279F6D78A646E71BCCE12CAB8B3B3E3/2]},
client_secret: {AshAuthentication.SecretFunction,
[
fun: &Example.User.client_secret_0_generated_1279F6D78A646E71BCCE12CAB8B3B3E3/2
]},
icon: :auth0,
id_token_signed_response_alg: nil,
id_token_ttl_seconds: nil,
identity_relationship_name: :identities,
identity_relationship_user_id_attribute: :user_id,
identity_resource: false,
name: :auth0,
nonce: false,
openid_configuration_uri: nil,
openid_configuration: nil,
private_key: nil,
provider: :oauth2,
redirect_uri: {AshAuthentication.SecretFunction,
[
fun: &Example.User.redirect_uri_0_generated_1279F6D78A646E71BCCE12CAB8B3B3E3/2
]},
register_action_name: :register_with_auth0,
registration_enabled?: true,
resource: Example.User,
sign_in_action_name: :sign_in_with_auth0,
site: {AshAuthentication.SecretFunction,
[fun: &Example.User.site_0_generated_1279F6D78A646E71BCCE12CAB8B3B3E3/2]},
strategy_module: AshAuthentication.Strategy.Auth0,
token_url: "/oauth/token",
trusted_audiences: nil,
user_url: "/userinfo"
}
(this example is taken from the dev/test examples)
It contains all the information needed for AA to figure out how to generate routes, actions and plugs for sign-in and registration. AA.Phoenix also uses it to figure out how to render sign-in and register pages for you. In the case of OAuth2-backed strategies they’re little more than links to the strategy’s request phase.
For example, if we take this strategy and pass it into AA.Strategy.routes/1
we can see that it generates the following routes which would be injected into your Phoenix router by AA.Phoenix.Router.auth_routes_for/2
:
iex> Example.User |> AshAuthentication.Info.strategy!(:auth0) |> AshAuthentication.Strategy.routes()
[{"/user/auth0", :request}, {"/user/auth0/callback", :callback}]
Actions
That’s not all we need however, as the OAuth2 strategy needs there to be a create action that can register the user or a read action which can sign-in a previously registered user, depending on whether registration_enabled?
is set. The actions for OAuth2 are documented here.
You can see from the strategy their names are inferred if not directly configured and AA will give you a compilation error if they are not present or are missing necessary stuff.
In the example we have a register_with_auth0
action defined as:
create :register_with_auth0 do
argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false
upsert? true
upsert_identity :username
change AshAuthentication.GenerateTokenChange
change Example.GenericOAuth2Change
change AshAuthentication.Strategy.OAuth2.IdentityChange
end
Here we’re telling Ash that we want to upsert a user (ie create it if it’s not already there) and that we want to use the named identity (ie unique constraint) to manage conflicts.
The Example.GenerateOAuth2Change
is used in test and simply tries using "nickname"
, "login"
and "preferred_username"
from the user_info
in order to set as the username.
Tokens
AA uses JWT for a number of purposes, which are generated and validated by joken. In AA all tokens are JWTs and not just random strings - this is a big difference between AA and phx_gen_auth
).
When a user registers or signs in by any strategy and tokens are enabled AA.GenerateTokenChange
generates a JWT which refers to the user and the purpose for which it was generated (usually just "user"
, but we also use them for resets, and other things). The generated JWT is stored in the record’s metadata so that it can be returned to the success
callback of the AuthController
and used in whatever way you see fit.
TokenResource
The token resource stores information about tokens - but by default only stores the JTI’s of any tokens you revoke. You can change this to by setting store_all_tokens?
to true in the token DSL, which effectively inverts the logic - any JTI that’s not present in the resource is considered revoked. This can be handy for features like global logout.
UserIdentity
Has two use cases:
- You want to have the user signed in to multiple OAuth providers at once, or
- You want AshAuthentication to automatically refresh an access token using a refresh token.
I can go into this more if you want.
Authentication
For OAuth2 based strategies (including OIDC) you send the user to the generated route to the :request
route (ie "<wherever I mounted it in my router>/user/auth0"
). The OAuth2 strategy’s request plug generates the required configuration to ask assent to generate a redirect URL, and then redirects the user to that URL.
At some point later on the user should be redirected back to the :callback
route which asks assent to validate the callback and then calls either your register or sign in actions with the appropriate arguments. If everything goes to plan you’ll receive a call to your AuthController’s success/4
callback with the conn, activity (activity is a tuple of the strategy name and the request phase), user and their shiny new token (or nil
). If not the failure
callback will be called.
Summary
I know this is long and I feel like I’ve just scratched the surface so please ask if you’d like me to dig into any details.