Using Auth0 with Tauri
Tags: tauri, authentication
Published: 2023-05-06 21:18:31

When I first went to integrate Auth0 with my Tauri app, I followed the [Electron Auth0 guide](https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/).

This approach tells you to create another Electron window and serve the authentication page in it. Then to intercept the callback redirect and extract the code from the URL. I was trying to adapt this to Tauri but running into a wall: You can't intercept navigations for external content in Tauri. 1.3 added an `on_navigation` handler for the `WindowBuilder` but that just did not work, and the `AppHandle` was not accessible from the registered closure.

At this point I just had to back up and figure out how to do this without trying to load the auth page in a WebView. The [best practices for a while now have encouraged running authentication through a browser](https://auth0.com/blog/oauth-2-best-practices-for-native-apps/). Authenticating in the browser has a couple of key advantages:

* It lets the user re-use signed in state for 3rd party authentication providers like Google/Facebook, potentially eliminating the need for them to type in any username or password
* It provides better security for the user because they don't need to type in passwords in an app, which could potentially host a lookalike site that is just harvesting their Google password

Thankfully it turns out it's relatively straightforward to do it correctly here.

## Send to the browser instead
The guide tells you how to generate a login URL. Instead of trying to load it ourselves, we need to hand it off to the default browser. From JS you'd use the [open](https://tauri.app/v1/api/js/shell#open) from the shell API. From the backend you can use the [webbrowser crate](https://crates.io/crates/webbrowser).

In the meantime the app can display a message asking them to check their browser and complete the authentication there.

## Return URL
You'll need to update the return URL to send you back to your app instead. This URI can be something like `myapp:auth`, where `myapp` is your app's registered protocol.

1) Change the return URL embedded in the auth URL, by changing the redirect_uri parameter.
2) Update the Application configuration in Auth0 to allow the new callback URI.

![Auth0 config example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xx49e410dwdnew5ps82r.png)

After the authentication succeeds in the browser, it will invoke `myapp:auth?code=abc...`, which can activate your waiting app.

You can also configure this to callback URL to your own web page, which can then redirect to your app protocol. This way would be the best user experience, as you have a message on this page: "You are authenticated, you may close this tab".

## Build the auth URL
Here's a snippet that builds the URL to send to the browser:

```rust
pub fn get_auth_url() -> String {
    let audience: String = urlencoding::encode(API_IDENTIFIER).into_owned();
    let scope: String = urlencoding::encode("openid profile offline_access").into_owned();
    let redirect_uri_encoded: String = urlencoding::encode(REDIRECT_URI).into_owned();
    format!("https://{AUTH0_DOMAIN}/authorize?audience={audience}&scope={scope}&response_type=code&client_id={CLIENT_ID}&redirect_uri={redirect_uri_encoded}")
}
```

## Protocol handler
You can use the [tauri-plugin-deep-link crate](https://crates.io/crates/tauri-plugin-deep-link) to register your app as a protocol handler. After you get your code, you can exchange it for an auth token in the same manner as the Electron guide, but for Rust you can use [reqwest](https://crates.io/crates/reqwest) for the HTTP call.

This example uses error_stack, but it should serve as a reference.

```rust
#[derive(Deserialize)]
struct TokenExchangeResponse {
    access_token: String,
    id_token: String,
    refresh_token: String,
}

#[derive(Debug)]
pub enum AuthError {
    RefreshTokenMissing,
    Http,
    InvalidUrl,
    InvalidJson,
}

pub async fn load_tokens(&mut self, callback_url: &str) -> Result<(), AuthError> {
    let parsed_url = Url::parse(callback_url).into_report().change_context(AuthError::InvalidUrl)?;
    let hash_query: HashMap<_, _> = parsed_url.query_pairs().into_owned().collect();
    let code = hash_query.get("code").ok_or(report!(AuthError::InvalidUrl))?;

    let token_exchange_body = json!({
        "grant_type": "authorization_code",
        "client_id": CLIENT_ID,
        "code": code,
        "redirect_uri": REDIRECT_URI
    })
    .to_string();

    let client = reqwest::Client::new();
    let response = client
        .post(format!("https://{AUTH0_DOMAIN}/oauth/token"))
        .header(reqwest::header::CONTENT_TYPE, "application/json")
        .body(token_exchange_body)
        .send()
        .await
        .into_report()
        .change_context(AuthError::Http)?;

    let response_text = response.text().await.into_report().change_context(AuthError::Http)?;
    let response_object: TokenExchangeResponse =
        serde_json::from_str(&response_text).into_report().change_context(AuthError::InvalidJson)?;

    // Tokens are in response_object. You can store them here.

    Ok(())
}
```

## Store credentials with keyring
You can store the token with the [keyring crate](https://crates.io/crates/keyring).

The token refresh works the same as in the official guide.