// A worked OAuth2 password-bearer app — FastAPI's security tutorial in explicit
// MoonBit form. `POST /token` reads the password form, checks the demo
// credentials, and issues a scoped HS256 JWT; `GET /users/me` is protected by a
// bearer `Security` with no scope requirement; `GET /users/me/items` additionally
// requires the `items` scope. The verification clock is injected (`now`) so the
// same app runs deterministically in a test and against the wall clock in
// production.
///|
/// Check the demo credentials. A real app would look the user up and verify a
/// password hash; here one hard-coded account keeps the example self-contained.
fn authenticate_user(username : String, password : String) -> Bool {
username == "alice" && password == "wonderland"
}
///|
/// Build the OAuth2 demo application. `secret` is the shared HS256 key; `now`
/// supplies the current Unix time (seconds) for both issuing and verifying, so a
/// caller controls time in tests. Tokens live for one hour.
pub fn oauth2_app(now : () -> Int64, secret? : String = "demo-secret") -> App {
let app = App::new()
let scheme = OAuth2PasswordBearer::new("/token", secret)
app.post(
"/token",
ctx => {
match ctx.oauth2_password_form() {
None => unprocessable([ValidationError::missing(["body", "username"])])
Some(form) =>
if authenticate_user(form.username, form.password) {
let token = create_access_token(
form.username,
secret,
now(),
scopes=form.scopes,
)
token_response(token)
} else {
let body : Map[String, Json] = Map([
("detail", "Incorrect username or password".to_json()),
])
@moonasgi.Response::new(
401,
[
("content-type", "application/json"),
("www-authenticate", "Bearer"),
],
@utf8.encode(body.to_json().stringify()),
)
}
}
},
summary="issue an access token",
)
app.get(
"/users/me",
ctx => {
match scheme.authenticate(ctx, now()) {
Err(resp) => resp
Ok(user) => {
let scopes : Array[Json] = []
for s in user.scopes {
scopes.push(s.to_json())
}
let body : Map[String, Json] = Map([
("username", user.subject.to_json()),
("scopes", scopes.to_json()),
])
json(200, body.to_json())
}
}
},
summary="the current user",
)
app.get(
"/users/me/items",
ctx => {
match scheme.authenticate(ctx, now(), scopes=["items"]) {
Err(resp) => resp
Ok(user) => {
let body : Map[String, Json] = Map([
("owner", user.subject.to_json()),
("items", ["hammer", "nail"].to_json()),
])
json(200, body.to_json())
}
}
},
summary="the current user's items (scope: items)",
)
app
}