Tauri + oauth2
Tags: tauri, webdev, tutorial, programming
Published: 2023-04-19 16:52:24

So you want to do native application authentication. 

Before we begin, I recommend you don't just skim the code snippets, many decisions are made due to _my own_ restrictions, **security concerns**, and lack of skill with rust.

I am not going to explain the mechanisms used in depth, as it would put both you and I to sleep.

## Auth flow

A quick read through the fascinating and very stimulating oauth0 specification that I am sure you read in full will tell you how native app auth is recommended. Machine to Machine auth is problematic, and some flows are more secure than others. 
**I chose to authenticate through the browser, secured using CSRF and PKCE**.

Meaning the flow is as follows:

1. Determine that user needs to authenticate
2. Call the `/authorize` endpoint to receive the `auth_url` with a PKCE challenge
3. Open the users browser allowing them to log in
4. Catch callback from idp 
5. Check CSRF integrity
6. Exchange the `code` from the callback with a token from `/oauth/token`
7. ????
8. :tada: Profit! :tada:


## Setup

> I will be using Auth0 as my authentication provider. But should work just the same with any provider.

First you should have a project.
```sh
$ pnpm create tauri-app 
```
You can pick whatever, we're going to touch only the rust side for added security :wink:

Some thing to remember, unlike web apps, we can't hide any secrets from a malicious (or curious, not gonna judge) actor. We also do not work in an isolated environment, so we should assume that all outward communication is entirely public. This means **NO CLIENT SECRET** my dudes, I mean it. 

We need a client id, authorization url, and a token url. You get those from your auth provider.
For example, in auth0 I need to create a new `Native Application`. Then inside I can take the `Client Id` and the `Domain`. Those are not secret, I put them in my env vars, but they can live wherever you want

```sh
OAUTH2_CLIENT_ID=<my client id>
OAUTH2_AUTH_URL=https://<domain>/authorize
OAUTH2_TOKEN_URL=https://<domain>/oauth/token
```
note that these endpoints are the standard, but they might be different for you.

now to make my life easier, I'm going to use a few crates. If you're a :crab: and know better, go ahead, I'm not your dad.

```toml
[dependencies]
tauri = { version = "1.2", features = ["window-close", "window-hide", "window-maximize", "window-minimize", "window-show", "window-start-dragging", "window-unmaximize"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
axum = { version = "0.6.12", features = ["headers"] }
oauth2 = "4.3"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json"] }
open = "4.0.2"
```

Because this post isn't long enough, I'm going to break down the usage:

- `tokio` -- Async
- `axum` -- The server framework
- `oauth2` -- The oauth2 library
- `reqwest` -- http request libary, playes well with oauth2
- `open` -- to open the browser without hassle
 
## Implement auth flow

Now that everything is in place, first lets create a struct to describe our auth dependencies
```rs
#[derive(Clone)]
struct AuthState {
    csrf_token: CsrfToken,
    pkce: Arc<(PkceCodeChallenge, String)>,
    client: Arc<BasicClient>,
    socket_addr: SocketAddr
}
```

and initiate them in the `main` method.

_Side note: I'm not going to include all my imports. Just follow your heart (LSP auto-import)_

```rs

#[tauri::command]
async fn authenticate() {
    todo!("we'll get here soon enough")
}

fn main() {
    let (pkce_code_challenge,pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
    let socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9133); // or any other port
    let redirect_url = format!("http://{socket_addr}/callback").to_string();
    
    let state = AuthState {
        csrf_token: CsrfToken::new_random(),
        pkce: Arc::new((pkce_code_challenge, PkceCodeVerifier::secret(&pkce_code_verifier).to_string())),
        client: Arc::new(create_client(RedirectUrl::new(redirect_url).unwrap())),
        socket_addr
    };

    tauri::Builder::default()
        .manage(state)
        .invoke_handler(tauri::generate_handler![authenticate])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```
Now, if you're using a more compliant provider than auth0, you can use a more robust and secure method to get the `socket_addr` like the oauth2 spec recommends

```rs
fn get_available_addr() -> SocketAddr {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    drop(listener);
    
    addr
}

fn main() {
    // ...
    let socket_addr = get_available_addr();
    // ...
}
```
This will make sure you are getting an unknowable available port. Making your server harder to mess with.

You will notice that I lower the `PkceCodeVerifier` to a `String`. This is because it does not implement `Clone` or `Copy` out of security concerns and it makes it really hard to pass around. For me this is safe enough, but you're free to do this your way.

the `create_client` function is exactly what it says on the tin. My implementation looks like this:

```rs
fn create_client(redirect_url: RedirectUrl) -> BasicClient {
    let client_id = ClientId::new(env!("OAUTH2_CLIENT_ID", "Missing AUTH0_CLIENT_ID!").to_string());

    let auth_url = AuthUrl::new(env!("OAUTH2_AUTH_URL", "Missing AUTH0_AUTH_URL!").to_string());

    let token_url = TokenUrl::new(env!("OAUTH2_TOKEN_URL", "Missing AUTH0_TOKEN_URL!").to_string());

    BasicClient::new(client_id, None, auth_url.unwrap(), token_url.ok())
        .set_redirect_uri(redirect_url)
}
```

now back to `authenticate`, we need to get the state from `tauri`. It does expose some `State` helper, but I found that passing around an `AppHandle` around is much more convenient to work with

```rs
#[tauri::command]
async fn authenticate(handle: tauri::AppHandle) {
    let auth = handle.state::<AuthState>();
}
```

Now with the state, we can create our auth url. Don't forget to add all the data you need like scopes and such.

```rs
#[tauri::command]
async fn authenticate(handle: tauri::AppHandle) {
    let auth = handle.state::<AuthState>();
    
    // The 2nd element is the csrf token.
    // We already have it so we don't care about it.
    let (auth_url, _) = auth
        .client
        .authorize_url(|| auth.csrf_token.clone())
        // .add_scope(...)
        .set_pkce_challenge(auth.pkce.0.clone())
        .url();
}
```

Before we open the browser with our newly-created url, we should spawn a server to actually listen to the callback. For that I defined a `run_server` function

```rs
async fn authorize() -> impl IntoResponse {
  todo!("woo hoo!")
}

async fn run_server(
    handle: tauri::AppHandle,
) -> Result<(), axum::Error> {
    let app = Router::new()
        .route("/callback", get(authorize))
        .layer(Extension(handle.clone()));

    let _ = axum::Server::bind(&handle.state::<AuthState>().socket_addr.clone())
        .serve(app.into_make_service())
        .await;

    Ok(())
}
```

and spawn it

```rs
#[tauri::command]
async fn authenticate(handle: tauri::AppHandle) {
    // ...
    let server_handle = tauri::async_runtime::spawn(async move { run_server(handle).await });
}
```

Now it's time to open the browser with the auth link using the `open` crate we added or some other method.

```rs
#[tauri::command]
async fn authenticate(handle: tauri::AppHandle) {
    // ...
    open::that(auth_url.to_string()).unwrap();
}
```

lets get back to `authorize` to implement it. 
The request we are expecting to receive is a `GET` request to the `/callback` endpoint with `code` and `state` (<- CSRF).

Thankfully, `axum` makes my life a bit easier

```rs
#[derive(Deserialize)]
struct CallbackQuery {
    code: AuthorizationCode,
    state: CsrfToken,
}

async fn authorize(query: Query<CallbackQuery>) -> impl IntoResponse {
    todo!("very cool, thanks axum")
}
```

We will also need the `AppHandle` we used earlier to access the state

```rs
async fn authorize(
    handle: Extension<tauri::AppHandle>,
    query: Query<CallbackQuery>
) -> impl IntoResponse {
    let auth = handle.state::<AuthState>();
}
```
Now we just gotta check the CSRF token, and exchange our `code` with the actual token, don't forget to attach the PKCE verifier.

```rs
async fn authorize(
    handle: Extension<tauri::AppHandle>,
    query: Query<CallbackQuery>
) -> impl IntoResponse {
    let auth = handle.state::<AuthState>();
    
    if query.state.secret() != auth.csrf_token.secret() {
        println!("Suspected Man in the Middle attack!");
        return "authorized".to_string(); // never let them know your next move
    }

    let token = auth
        .client
        .exchange_code(query.code.clone())
        .set_pkce_verifier(PkceCodeVerifier::new(auth.pkce.1.clone()))
        .request_async(async_http_client)
        .await
        .unwrap();

    "authorized".to_string()
}
```

Ok now what? You tell me. You have the token!
you can use [`keyring`](https://crates.io/crates/keyring) to store the token, you can use a `channel` to broadcast back to `authenticate` that you got a token so it can close the server (which is what I did). The world is your authenticated oyster :)

### Closing words

Despite the length of this post, this solution is not complete! You, the reader, will have to implement token refresh and usage, you will have to implement server control, you will have to configure your oauth2 provider with the correct parameters. But for the sake of brevity I kept it to the absolute bare essentials.

If there is a missing piece that you think is absolutely essential or an optimization feel free to drop a comment.