# Stackure Rust SDK
[](https://github.com/syi-stackure/sdk-rust/actions/workflows/check-build.yml)
[](https://crates.io/crates/stackure)
[](https://docs.rs/stackure)
[](./LICENSE)
Passwordless magic-link authentication SDK for Rust — drop-in tower middleware for axum, tonic, and hyper.
Protect an app with one line, or verify sessions and send magic links directly against the [Stackure](https://stackure.com) auth API.
## Install
```toml
[dependencies]
stackure = "1"
```
Requires Rust 2024 edition.
## Configure
```bash
export STACKURE_APP_SECRET=... # from the app page in Stackure, shown once
```
Sent as `X-App-Secret` on every call except the sign-out made by `logout`, which carries the user's session token instead. The first call that needs it fails with `StackureError::Validation` when it is unset. `STACKURE_BASE_URL` optionally overrides the API host.
A newly registered app is not usable by anyone, even its creator, until it is shared with the organization or assigned to a team in Stackure. Do that before testing sign-in.
## Protect an app
```rust
use stackure::{auth, user_from_request};
const APP_ID: &str = "7f3c1a2e-9b4d-4e6f-8a1b-2c3d4e5f6071"; // your app's UUID in Stackure
let app = Router::new()
.route("/admin", get(handler))
.layer(auth(APP_ID, &["can_approve_invoice"]));
```
Access the authenticated user in your handler:
```rust
let user = user_from_request(&parts).unwrap();
println!("{} {} {:?}", user.user_email, user.account_id, user.user_permissions);
```
In axum you can also take an `Extension<User>` directly.
- API requests get JSON errors
- Browser requests get redirected to sign-in
- The sign-in handoff is automatic: Stackure POSTs a `session_token` (an app-scoped session token valid only for this app) back to your app, the layer validates it and stores it as a cookie on your domain. Handoff bodies over 4 KB are ignored
## Requirements
Sessions are not bound to the browser's user agent or IP. The SDK still
forwards the original `User-Agent` and `X-Forwarded-For` when validating from
your server, but they are informational only.
When `X-Forwarded-For` is absent the SDK falls back to the peer address, read
from axum's `ConnectInfo<SocketAddr>`. Serve with
`into_make_service_with_connect_info` to make it available, or disable the
default `axum` feature if you do not need it.
The app cookie is marked `Secure` only when the request arrived over TLS as far as the SDK can tell: send `X-Forwarded-Proto: https` from your TLS terminator, since a direct TLS listener is not visible from the request itself.
Every request with a session token is validated against Stackure, so revocation
is immediate. Requests without a well-formed token get the sign-in URL without a
Stackure call.
Every call has one 2-second wall-clock deadline covering connect, headers, body
and the single retry. The SDK retries once, after 500 ms, on a 5xx response or a
connection failure, and only if more than 500 ms of the deadline remain. A
timeout, including while reading the body, is `StackureError::Timeout` and is
never retried.
## Verify manually
```rust
let result = stackure::verify(APP_ID, &parts, &["can_approve_invoice"]).await;
if !result.authenticated {
let error = result.error.unwrap();
// error.code, error.message, error.sign_in_url
}
// result.user
```
`verify` never returns an error — transport and API failures come back as a
500 result.
## Send a magic link
```rust
let response = stackure::send_magic_link("user@example.com", Some(APP_ID)).await?;
// response.message
```
## Log out
```rust
async fn logout(parts: Parts) -> Response<Body> {
stackure::logout(&parts).await
}
let app = Router::new().route("/logout", any(logout));
```
```html
<form method="post" action="/logout"><button>Sign out</button></form>
```
Mount it for every method on the logout path, as `any` does here. Trigger it
with a form or button that POSTs from the app's own page; a link or any other
request is sent to Stackure's sign-out page, where the user confirms.
A request from the app's own page is a POST with `Sec-Fetch-Site: same-origin`
or, when that header is absent, with an `Origin` whose host and port match
`Host` and whose scheme is `https` if the request arrived over HTTPS. A request
that repeats `Sec-Fetch-Site`, `Origin` or `Host` never counts. It ends the
user's access everywhere with a server-side call to Stackure, clears the app's
cookie and returns a 303 to Stackure. If the call fails, the 303 goes to
Stackure's sign-out page instead, where the user can finish signing out.
`logout` is asynchronous: awaiting it yields the `Response<B>`, never an error.
## Errors
Everything except `verify` and `logout` returns `StackureError`. `logout` never
returns an error: a failed sign-out call still ends in a redirect. Match on the
variant, or call `.code()` for the same category string the other Stackure SDKs
expose as `.code`:
```rust
use stackure::StackureError;
match stackure::send_magic_link(email, None).await {
Err(StackureError::Validation(m)) => {} // bad input
Err(StackureError::Auth(m)) => {} // 401 from the API
Err(StackureError::Forbidden(m)) => {} // 403 from the API
Err(StackureError::Timeout(m)) => {} // exceeded the 2s deadline
Err(StackureError::Network(m)) => {} // everything else
Ok(response) => {}
}
```
## Dependencies
Rust's standard library has no HTTP client and no TLS, so unlike the Go, JavaScript,
and Python SDKs this one cannot be dependency-free. It builds directly on `hyper`
and `rustls` — the stack axum and tonic already run on — rather than on a
higher-level client, so in a typical axum app it adds around twenty crates.
## Contributing
Open a PR.
## Security
Report vulnerabilities via [GitHub Security Advisories](https://github.com/syi-stackure/sdk-rust/security/advisories/new). Releases publish to crates.io via OIDC trusted publishing with [GitHub build-provenance attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
## License
MIT