This commit is contained in:
parent
c47a3fb5fc
commit
f32e69be99
4 changed files with 219 additions and 110 deletions
|
|
@ -117,7 +117,10 @@ pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String> {
|
|||
bcrypt::hash(password, 10).ok()
|
||||
}
|
||||
|
||||
pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
pub async fn read_basic_auth_token(
|
||||
connection: &Pool,
|
||||
token: &str,
|
||||
) -> Option<(User, Option<[u8; 32]>)> {
|
||||
let data = match BASE64_STANDARD.decode(&token) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
|
|
@ -161,13 +164,16 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
|
|||
let password_valid = user.verify_cleartext_password(&login_password);
|
||||
|
||||
if password_valid == Some(true) {
|
||||
Some(user)
|
||||
Some((user, None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
pub async fn read_bearer_auth_token(
|
||||
connection: &Pool,
|
||||
token: &str,
|
||||
) -> Option<(User, Option<[u8; 32]>)> {
|
||||
let data = TokenData::decode(token)?;
|
||||
|
||||
let token_info = sqlx::query!(
|
||||
|
|
@ -189,7 +195,7 @@ pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<Us
|
|||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(user)
|
||||
Some((user, token_info.cert_hash.and_then(|v| v.try_into().ok())))
|
||||
}
|
||||
|
||||
pub fn generate_nex_password() -> String {
|
||||
|
|
@ -214,6 +220,7 @@ pub fn generate_nex_password() -> String {
|
|||
|
||||
pub struct Auth<const FORCE_BEARER_AUTH: bool = true, const USE_CERT: bool = FORCE_BEARER_AUTH>(
|
||||
pub User,
|
||||
pub Option<[u8; 32]>,
|
||||
);
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> AsRef<User>
|
||||
|
|
@ -278,6 +285,7 @@ pub async fn link_certificate_to_pid(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// todo: make this more consistent by requiring a cert to be asociated to the token if USE_CERT is set
|
||||
#[async_trait]
|
||||
impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
|
|
@ -297,13 +305,13 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
|
||||
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
|
||||
|
||||
let user = match auth_type {
|
||||
let data = match auth_type {
|
||||
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
|
||||
"Bearer" => read_bearer_auth_token(pool, token).await,
|
||||
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
|
||||
};
|
||||
|
||||
let Some(user) = user else {
|
||||
let Some((user, cert)) = data else {
|
||||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
|
||||
|
|
@ -316,7 +324,7 @@ impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
|||
// ..user
|
||||
// };
|
||||
|
||||
Outcome::Success(Self(user))
|
||||
Outcome::Success(Self(user, cert))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
use crate::Pool;
|
||||
use serde::Serialize;
|
||||
use rocket::FromForm;
|
||||
use sha2::{Sha256, Digest};
|
||||
use bytemuck::bytes_of;
|
||||
use rocket::{post, State, form::Form, http::Status, serde::json::Json};
|
||||
use sqlx::Row;
|
||||
use std::env;
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::nnid::oauth::generate_token::create_token;
|
||||
use crate::nnid::oauth::generate_token::token_type::AUTH_TOKEN;
|
||||
use bytemuck::bytes_of;
|
||||
use chrono::Utc;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::FromForm;
|
||||
use rocket::{State, form::Form, http::Status, post, serde::json::Json};
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::Row;
|
||||
use std::env;
|
||||
|
||||
pub static CLIENT_SECRET: Lazy<String> = Lazy::new(|| {
|
||||
env::var("OAUTH_CLIENT_SECRET").expect("OAUTH_CLIENT_SECRET not set")
|
||||
});
|
||||
pub static CLIENT_SECRET: Lazy<String> =
|
||||
Lazy::new(|| env::var("OAUTH_CLIENT_SECRET").expect("OAUTH_CLIENT_SECRET not set"));
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OAuthTokenResponse {
|
||||
|
|
@ -53,16 +52,17 @@ pub struct OAuthErrorResponse {
|
|||
#[post("/api/v2/oauth2/generate_token", data = "<form_data>")]
|
||||
pub async fn generate_token(
|
||||
pool: &State<Pool>,
|
||||
form_data: Form<TokenRequest<'_>>
|
||||
form_data: Form<TokenRequest<'_>>,
|
||||
) -> Result<Json<OAuthTokenResponse>, (Status, Json<OAuthErrorResponse>)> {
|
||||
|
||||
match form_data.client_id {
|
||||
Some("account") | Some("splatnet") => {
|
||||
let cl_secret: String = CLIENT_SECRET.clone();
|
||||
if form_data.client_secret != Some(&cl_secret) {
|
||||
return Err((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +72,9 @@ pub async fn generate_token(
|
|||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_client".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_client".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -82,11 +84,15 @@ pub async fn generate_token(
|
|||
"password" => {
|
||||
let username = form_data.username.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
let password = form_data.password.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
let user_row = match sqlx::query("SELECT pid, password FROM users WHERE username = $1")
|
||||
|
|
@ -95,10 +101,14 @@ pub async fn generate_token(
|
|||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => return Err((
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
)),
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let db_pid: i32 = user_row.get("pid");
|
||||
|
|
@ -107,31 +117,39 @@ pub async fn generate_token(
|
|||
if !verify_nintendo_password(db_pid, password, &db_bcrypt_hash) {
|
||||
return Err((
|
||||
Status::Unauthorized,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
db_pid
|
||||
},
|
||||
}
|
||||
|
||||
"authorization_code" => {
|
||||
let incoming_code = form_data.code.ok_or((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_request".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_request".to_string(),
|
||||
}),
|
||||
))?;
|
||||
|
||||
let code_row = match sqlx::query(
|
||||
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1"
|
||||
"SELECT pid, expires_at, used FROM oauth_auth_codes WHERE code = $1",
|
||||
)
|
||||
.bind(incoming_code)
|
||||
.fetch_optional(pool.inner())
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => return Err((
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
)),
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let is_used: bool = code_row.get("used");
|
||||
|
|
@ -141,7 +159,9 @@ pub async fn generate_token(
|
|||
if is_used || expires_at < Utc::now().naive_utc() {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "invalid_grant".to_string() })
|
||||
Json(OAuthErrorResponse {
|
||||
error: "invalid_grant".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -151,15 +171,19 @@ pub async fn generate_token(
|
|||
.await;
|
||||
|
||||
target_pid
|
||||
},
|
||||
}
|
||||
|
||||
_ => return Err((
|
||||
_ => {
|
||||
return Err((
|
||||
Status::BadRequest,
|
||||
Json(OAuthErrorResponse { error: "unsupported_grant_type".to_string() })
|
||||
))
|
||||
Json(OAuthErrorResponse {
|
||||
error: "unsupported_grant_type".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None).await;
|
||||
let token = create_token(pool.inner(), pid, AUTH_TOKEN, None, None).await;
|
||||
|
||||
Ok(Json(OAuthTokenResponse {
|
||||
access_token: token,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
#![allow(unused)]
|
||||
use rocket::{post, FromForm, State};
|
||||
use rocket::form::Form;
|
||||
use serde::{Serialize};
|
||||
use crate::Pool;
|
||||
use crate::account::account::{Auth, DeviceCert, User, link_certificate_to_pid};
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::agreements::{CFIP, EVIL_AGREEMENT_THING};
|
||||
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
|
||||
use crate::nnid::oauth::TokenData;
|
||||
use crate::Pool;
|
||||
use crate::nnid::oauth::generate_token::token_type::{AUTH_REFRESH_TOKEN, AUTH_TOKEN};
|
||||
use crate::xml::Xml;
|
||||
use rocket::form::Form;
|
||||
use rocket::{FromForm, State, post};
|
||||
use serde::Serialize;
|
||||
|
||||
pub mod token_type {
|
||||
pub const AUTH_REFRESH_TOKEN: i32 = 1;
|
||||
|
|
@ -17,31 +17,24 @@ pub mod token_type{
|
|||
}
|
||||
|
||||
const ACCOUNT_ID_OR_PASSWORD_ERRORS: Errors = Errors {
|
||||
error: &[
|
||||
Error{
|
||||
error: &[Error {
|
||||
code: "0106",
|
||||
message: "Invalid account ID or password"
|
||||
}
|
||||
]
|
||||
message: "Invalid account ID or password",
|
||||
}],
|
||||
};
|
||||
|
||||
const ACCOUNT_BANNED_ERRORS: Errors = Errors {
|
||||
error: &[
|
||||
Error{
|
||||
error: &[Error {
|
||||
code: "0108",
|
||||
message: "Account banned from server"
|
||||
}
|
||||
]
|
||||
message: "Account banned from server",
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
const REREAD_EULA_EXTRABANNED_ERRORS: Errors = Errors {
|
||||
error: &[
|
||||
Error{
|
||||
error: &[Error {
|
||||
code: "0109",
|
||||
message: "REREAD THE EULA LOL"
|
||||
}
|
||||
]
|
||||
message: "REREAD THE EULA LOL",
|
||||
}],
|
||||
};
|
||||
#[derive(FromForm)]
|
||||
pub struct TokenRequestData<'a> {
|
||||
|
|
@ -55,17 +48,27 @@ pub struct TokenRequestData<'a>{
|
|||
pub struct TokenReturnData {
|
||||
token: String,
|
||||
refresh_token: String,
|
||||
expires_in: i32
|
||||
expires_in: i32,
|
||||
}
|
||||
|
||||
pub async fn create_token(pool: &Pool, pid: i32, token_type: i32, title_id: Option<&str>) -> String{
|
||||
pub async fn create_token(
|
||||
pool: &Pool,
|
||||
pid: i32,
|
||||
token_type: i32,
|
||||
title_id: Option<&str>,
|
||||
cert_hash: Option<&[u8]>,
|
||||
) -> String {
|
||||
let data = sqlx::query!(
|
||||
"insert into tokens (token_type, pid, title_id)
|
||||
values ($1, $2, $3) returning token_id, random",
|
||||
token_type, pid, title_id
|
||||
"insert into tokens (token_type, pid, title_id, cert_hash)
|
||||
values ($1, $2, $3, $4) returning token_id, random",
|
||||
token_type,
|
||||
pid,
|
||||
title_id,
|
||||
cert_hash
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token_id = data.token_id;
|
||||
let random = data.random;
|
||||
|
|
@ -73,23 +76,33 @@ pub async fn create_token(pool: &Pool, pid: i32, token_type: i32, title_id: Opti
|
|||
let token = TokenData {
|
||||
token_id,
|
||||
random,
|
||||
pid
|
||||
pid,
|
||||
};
|
||||
|
||||
token.encode().to_string()
|
||||
}
|
||||
|
||||
|
||||
impl TokenReturnData {
|
||||
async fn new(pid: i32, pool: &Pool) -> Self {
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None).await;
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None, None).await;
|
||||
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None).await;
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, None).await;
|
||||
|
||||
Self {
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: 3600
|
||||
expires_in: 3600,
|
||||
}
|
||||
}
|
||||
async fn new_with_cert(pid: i32, cert: Option<&[u8]>, pool: &Pool) -> Self {
|
||||
let token = create_token(pool, pid, AUTH_TOKEN, None, None).await;
|
||||
|
||||
let refresh_token = create_token(pool, pid, AUTH_REFRESH_TOKEN, None, None).await;
|
||||
|
||||
Self {
|
||||
token,
|
||||
refresh_token,
|
||||
expires_in: 3600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,17 +110,26 @@ impl TokenReturnData {
|
|||
#[derive(Serialize)]
|
||||
#[serde(rename = "OAuth20")]
|
||||
pub struct TokenRequestReturnData {
|
||||
access_token: TokenReturnData
|
||||
access_token: TokenReturnData,
|
||||
}
|
||||
|
||||
#[post("/v1/api/oauth20/access_token/generate", data = "<data>")]
|
||||
pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>, ip: CFIP, cert: DeviceCert) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>>{
|
||||
pub async fn generate_token(
|
||||
pool: &State<Pool>,
|
||||
data: Form<TokenRequestData<'_>>,
|
||||
ip: CFIP,
|
||||
cert: DeviceCert,
|
||||
) -> Result<Xml<TokenRequestReturnData>, Option<Errors<'static>>> {
|
||||
let pool = pool.inner();
|
||||
|
||||
let user = User::get_by_username(data.user_id, pool).await
|
||||
let user = User::get_by_username(data.user_id, pool)
|
||||
.await
|
||||
.ok_or(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS))?;
|
||||
|
||||
if !user.verify_hashed_password(&data.password).is_some_and(|v| v){
|
||||
if !user
|
||||
.verify_hashed_password(&data.password)
|
||||
.is_some_and(|v| v)
|
||||
{
|
||||
return Err(Some(ACCOUNT_ID_OR_PASSWORD_ERRORS));
|
||||
}
|
||||
|
||||
|
|
@ -125,9 +147,8 @@ pub async fn generate_token(pool: &State<Pool>, data: Form<TokenRequestData<'_>>
|
|||
|
||||
link_certificate_to_pid(&pool, &cert.0, user.pid).await?;
|
||||
|
||||
let access_token = TokenReturnData::new(user.pid, pool).await;
|
||||
let access_token =
|
||||
TokenReturnData::new_with_cert(user.pid, Some(&cert.0.hash()[..]), pool).await;
|
||||
|
||||
Ok(Xml(TokenRequestReturnData{
|
||||
access_token
|
||||
}))
|
||||
Ok(Xml(TokenRequestReturnData { access_token }))
|
||||
}
|
||||
|
|
@ -4,10 +4,14 @@ use crate::error::{Error, Errors};
|
|||
use crate::nnid::oauth::generate_token::create_token;
|
||||
use crate::nnid::oauth::generate_token::token_type::NEX_TOKEN;
|
||||
use crate::xml::Xml;
|
||||
use log::{info, warn};
|
||||
use nex_account::grpc::Pid;
|
||||
use reqwest::header::SERVER;
|
||||
use rocket::{State, get};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome, Request};
|
||||
use rocket::{State, async_trait, get};
|
||||
use serde::Serialize;
|
||||
use sqlx::query;
|
||||
use sqlx::types::ipnetwork::IpNetwork::V4;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
|
|
@ -55,10 +59,52 @@ pub struct ServiceToken {
|
|||
token: String,
|
||||
}
|
||||
|
||||
pub async fn store_or_check_serial(pool: &Pool, serial: &str, cert_hash: [u8; 32]) -> bool {
|
||||
let Ok(res) = query!(
|
||||
"select serial from certificates where hash = $1",
|
||||
&cert_hash[..]
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
else {
|
||||
warn!(
|
||||
"user tried to access a route which is locked behind a console asociated token without a console token"
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(stored_serial) = res.serial else {
|
||||
query!(
|
||||
"update certificates set serial = $1 where hash = $2",
|
||||
serial,
|
||||
&cert_hash[..]
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
serial == stored_serial
|
||||
}
|
||||
|
||||
struct Serial(String);
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for Serial {
|
||||
type Error = Errors<'static>;
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let Some(header) = request.headers().get("X-Nintendo-Serial-Number").next() else {
|
||||
warn!("serial number wasnt sent on request which expected a serial number");
|
||||
return Outcome::Error((Status::BadRequest, SERVER_ERROR));
|
||||
};
|
||||
|
||||
Outcome::Success(Self(header.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/v1/api/provider/service_token/@me")]
|
||||
pub async fn get_service_token(
|
||||
pool: &State<Pool>,
|
||||
auth: Auth<true, false>,
|
||||
serial: Serial,
|
||||
) -> Result<Xml<ServiceToken>, Option<Errors<'static>>> {
|
||||
// just gonna put this here as a side note for the future:
|
||||
// we could also be using key derivation to derive the nex token as if it were a key
|
||||
|
|
@ -69,7 +115,17 @@ pub async fn get_service_token(
|
|||
|
||||
let pool = pool.inner();
|
||||
|
||||
let token = create_token(pool, auth.pid, NEX_TOKEN, None).await;
|
||||
let Some(cert_hash) = auth.1 else {
|
||||
info!("attempt to generate service token using non wii u/certificate token");
|
||||
return Err(Some(SERVER_ERROR));
|
||||
};
|
||||
|
||||
if !store_or_check_serial(pool, &serial.0, cert_hash).await {
|
||||
info!("serial mismatched with certificate");
|
||||
return Err(Some(SERVER_ERROR));
|
||||
}
|
||||
|
||||
let token = create_token(pool, auth.pid, NEX_TOKEN, None, Some(&cert_hash[..])).await;
|
||||
|
||||
Ok(Xml(ServiceToken { token }))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue