// Which scope to ask for. There is one order, and it is the protocol's
// (SEP-835): what the server asked for in its challenge first, because that is
// the scope that satisfies the request at hand; then what the resource says it
// supports; then whatever the client was configured with.
///|
/// The scope for an authorization request. `None` when nothing names one, in
/// which case the request carries no `scope` parameter at all: sending an empty
/// one is not the same thing to every server.
pub fn scope_for(
challenge : Challenge?,
resource : Resource?,
configured : String?,
) -> String? {
if challenge is Some(challenge) {
if challenge.scope is Some(scope) &&
!scope.trim(chars=" ").to_owned().is_empty() {
return Some(scope)
}
}
if resource is Some(resource) {
if resource.scopes_supported is Some(scopes) && !scopes.is_empty() {
return Some(scopes.join(" "))
}
}
configured
}
///|
/// Whether every scope `asked` names is one `granted` already carries. A
/// challenge that names no scope asks nothing new; one that names a scope this
/// client was never granted is a step up, and a refresh cannot answer it
/// (RFC 6749 §6 keeps a refresh inside the scope it was given).
pub fn scope_within(asked : String?, granted : String?) -> Bool {
guard asked is Some(asked) else { return true }
guard granted is Some(granted) else { return false }
let held : Array[String] = []
for piece in granted.split(" ") {
let scope = piece.to_owned()
if !scope.is_empty() && !held.contains(scope) {
held.push(scope)
}
}
for piece in asked.split(" ") {
let scope = piece.to_owned()
if !scope.is_empty() && !held.contains(scope) {
return false
}
}
true
}
///|
/// The scope to ask for when this client wants a refresh token. The
/// authorization server's own `offline_access` is what makes it hand one out,
/// and a server that does not list it must not be asked (SEP-2207). The
/// client-credentials grant gets no refresh token at all (RFC 6749 §4.4.3), so
/// nothing is added for it.
pub fn with_offline_access(
scope : String?,
server : Server,
grant : Grant,
) -> String? {
guard grant is AuthorizationCode else { return scope }
match server.scopes_supported {
Some(supported) =>
if supported.contains("offline_access") {
widen_scope(scope, Some("offline_access"))
} else {
scope
}
None => scope
}
}
///|
/// The union of two scope strings, the current one first, without repeats.
pub fn widen_scope(current : String?, asked : String?) -> String? {
guard asked is Some(asked) else { return current }
let seen : Array[String] = []
for at in 0..<2 {
let text = if at == 0 { current.unwrap_or("") } else { asked }
for piece in text.split(" ") {
let scope = piece.to_owned()
if !scope.is_empty() && !seen.contains(scope) {
seen.push(scope)
}
}
}
if seen.is_empty() {
None
} else {
Some(seen.join(" "))
}
}