OAuth 2.0 Authorization Server [MOD-559] (#733)

* WIP end-of-day push

* Authorize endpoint, accept endpoints, DB stuff for oauth clients, their redirects, and client authorizations

* OAuth Client create route

* Get user clients

* Client delete

* Edit oauth client

* Include redirects in edit client route

* Database stuff for tokens

* Reorg oauth stuff out of auth/flows and into its own module

* Impl OAuth get access token endpoint

* Accept oauth access tokens as auth and update through AuthQueue

* User OAuth authorization management routes

* Forgot to actually add the routes lol

* Bit o cleanup

* Happy path test for OAuth and minor fixes for things it found

* Add dummy data oauth client (and detect/handle dummy data version changes)

* More tests

* Another test

* More tests and reject endpoint

* Test oauth client and authorization management routes

* cargo sqlx prepare

* dead code warning

* Auto clippy fixes

* Uri refactoring

* minor name improvement

* Don't compile-time check the test sqlx queries

* Trying to fix db concurrency problem to get tests to pass

* Try fix from test PR

* Fixes for updated sqlx

* Prevent restricted scopes from being requested or issued

* Get OAuth client(s)

* Remove joined oauth client info from authorization returns

* Add default conversion to OAuthError::error so we can use ?

* Rework routes

* Consolidate scopes into SESSION_ACCESS

* Cargo sqlx prepare

* Parse to OAuthClientId automatically through serde and actix

* Cargo clippy

* Remove validation requiring 1 redirect URI on oauth client creation

* Use serde(flatten) on OAuthClientCreationResult
This commit is contained in:
Jackson Kruger
2023-10-30 11:14:38 -05:00
committed by GitHub
parent 8803e11945
commit 6cfd4637db
54 changed files with 3658 additions and 135 deletions

View File

@@ -103,6 +103,9 @@ bitflags::bitflags! {
// delete an organization
const ORGANIZATION_DELETE = 1 << 38;
// only accessible by modrinth-issued sessions
const SESSION_ACCESS = 1 << 39;
const NONE = 0b0;
}
}
@@ -118,6 +121,7 @@ impl Scopes {
| Scopes::PAT_DELETE
| Scopes::SESSION_READ
| Scopes::SESSION_DELETE
| Scopes::SESSION_ACCESS
| Scopes::USER_AUTH_WRITE
| Scopes::USER_DELETE
| Scopes::PERFORM_ANALYTICS
@@ -126,6 +130,19 @@ impl Scopes {
pub fn is_restricted(&self) -> bool {
self.intersects(Self::restricted())
}
pub fn parse_from_oauth_scopes(scopes: &str) -> Result<Scopes, bitflags::parser::ParseError> {
let scopes = scopes.replace(' ', "|").replace("%20", "|");
bitflags::parser::from_str(&scopes)
}
pub fn to_postgres(&self) -> i64 {
self.bits() as i64
}
pub fn from_postgres(value: i64) -> Self {
Self::from_bits(value as u64).unwrap_or(Scopes::NONE)
}
}
#[derive(Serialize, Deserialize)]
@@ -161,3 +178,64 @@ impl PersonalAccessToken {
}
}
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
#[test]
fn test_parse_from_oauth_scopes_well_formed() {
let raw = "USER_READ_EMAIL SESSION_READ ORGANIZATION_CREATE";
let expected = Scopes::USER_READ_EMAIL | Scopes::SESSION_READ | Scopes::ORGANIZATION_CREATE;
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
assert_same_flags(expected, parsed);
}
#[test]
fn test_parse_from_oauth_scopes_empty() {
let raw = "";
let expected = Scopes::empty();
let parsed = Scopes::parse_from_oauth_scopes(raw).unwrap();
assert_same_flags(expected, parsed);
}
#[test]
fn test_parse_from_oauth_scopes_invalid_scopes() {
let raw = "notascope";
let parsed = Scopes::parse_from_oauth_scopes(raw);
assert!(parsed.is_err());
}
#[test]
fn test_parse_from_oauth_scopes_invalid_separator() {
let raw = "USER_READ_EMAIL & SESSION_READ";
let parsed = Scopes::parse_from_oauth_scopes(raw);
assert!(parsed.is_err());
}
#[test]
fn test_parse_from_oauth_scopes_url_encoded() {
let raw = urlencoding::encode("PAT_WRITE COLLECTION_DELETE").to_string();
let expected = Scopes::PAT_WRITE | Scopes::COLLECTION_DELETE;
let parsed = Scopes::parse_from_oauth_scopes(&raw).unwrap();
assert_same_flags(expected, parsed);
}
fn assert_same_flags(expected: Scopes, actual: Scopes) {
assert_eq!(
expected.iter_names().map(|(name, _)| name).collect_vec(),
actual.iter_names().map(|(name, _)| name).collect_vec()
);
}
}