mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-02 16:20:57 +02:00
fix(security): validate extension callback state
This commit is contained in:
@@ -104,25 +104,32 @@ import Gobackend
|
||||
}
|
||||
|
||||
/// Extension return URLs:
|
||||
/// - OAuth: spotiflac://callback?code=...&state=<extension_id>
|
||||
/// - Signed session: spotiflac://session-grant?grant=...&state=<extension_id>
|
||||
/// - OAuth: spotiflac://callback?code=...&state=<one_time_nonce>
|
||||
/// - Signed session: spotiflac://session-grant?grant=...&state=<one_time_nonce>
|
||||
@discardableResult
|
||||
private func handleExtensionOAuthRedirect(url: URL) -> Bool {
|
||||
guard let route = ExtensionCallbackParser.parse(url) else { return false }
|
||||
streamQueue.async {
|
||||
var err: NSError?
|
||||
var response: String?
|
||||
guard let extensionId = GobackendConsumeExtensionCallbackState(
|
||||
route.state,
|
||||
&err
|
||||
), err == nil else {
|
||||
NSLog("SpotiFLAC Mobile: Rejected invalid or expired extension callback")
|
||||
return
|
||||
}
|
||||
if route.isSessionGrant {
|
||||
GobackendSetExtensionSessionGrantByID(route.extensionId, route.code)
|
||||
GobackendSetExtensionSessionGrantByID(extensionId, route.code)
|
||||
response = GobackendInvokeExtensionActionJSON(
|
||||
route.extensionId,
|
||||
extensionId,
|
||||
"completeGrant",
|
||||
&err
|
||||
)
|
||||
} else {
|
||||
GobackendSetExtensionAuthCodeByID(route.extensionId, route.code)
|
||||
GobackendSetExtensionAuthCodeByID(extensionId, route.code)
|
||||
response = GobackendInvokeExtensionActionJSON(
|
||||
route.extensionId,
|
||||
extensionId,
|
||||
"completeSpotifyLogin",
|
||||
&err
|
||||
)
|
||||
@@ -130,7 +137,7 @@ import Gobackend
|
||||
if err == nil && route.isSessionGrant {
|
||||
do {
|
||||
try self.requireSuccessfulExtensionAction(
|
||||
extensionId: route.extensionId,
|
||||
extensionId: extensionId,
|
||||
actionName: "completeGrant",
|
||||
response: response
|
||||
)
|
||||
@@ -140,11 +147,11 @@ import Gobackend
|
||||
}
|
||||
if let err = err {
|
||||
NSLog(
|
||||
"SpotiFLAC: Extension callback complete failed: \(err.localizedDescription)")
|
||||
"SpotiFLAC Mobile: Extension callback failed (code \(err.code))")
|
||||
} else if route.isSessionGrant {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.notifySessionGrantCompleted(
|
||||
extensionId: route.extensionId
|
||||
extensionId: extensionId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
struct ExtensionCallbackRoute: Equatable {
|
||||
let code: String
|
||||
let extensionId: String
|
||||
let state: String
|
||||
let isSessionGrant: Bool
|
||||
}
|
||||
|
||||
@@ -36,17 +36,17 @@ enum ExtensionCallbackParser {
|
||||
?? queryItems.first { $0.name == "code" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? ""
|
||||
let extensionId =
|
||||
let state =
|
||||
queryItems.first { $0.name == "state" }?.value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? ""
|
||||
|
||||
guard !code.isEmpty, !extensionId.isEmpty else {
|
||||
guard !code.isEmpty, !state.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return ExtensionCallbackRoute(
|
||||
code: code,
|
||||
extensionId: extensionId,
|
||||
state: state,
|
||||
isSessionGrant: isSessionGrant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class RunnerTests: XCTestCase {
|
||||
route,
|
||||
ExtensionCallbackRoute(
|
||||
code: "auth-code",
|
||||
extensionId: "spotify-web",
|
||||
state: "spotify-web",
|
||||
isSessionGrant: false
|
||||
)
|
||||
)
|
||||
@@ -30,7 +30,7 @@ class RunnerTests: XCTestCase {
|
||||
route,
|
||||
ExtensionCallbackRoute(
|
||||
code: "session-token",
|
||||
extensionId: "provider",
|
||||
state: "provider",
|
||||
isSessionGrant: true
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user