Skip to content

Commit 7594ef0

Browse files
authored
Merge pull request #147 from pusher/add-authorizer
Add authorizer
2 parents 6a542bd + 71cbb7a commit 7594ef0

9 files changed

+193
-18
lines changed

PusherSwift/PusherSwift.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
3389F5721CAEDDF300563F49 /* PusherChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3389F5711CAEDDF300563F49 /* PusherChannels.swift */; };
2020
3389F5761CAEDE2800563F49 /* PusherGlobalChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3389F5751CAEDE2800563F49 /* PusherGlobalChannel.swift */; };
2121
3389F57A1CAEDEC800563F49 /* PusherClientOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3389F5791CAEDEC800563F49 /* PusherClientOptions.swift */; };
22+
3390D1E61F054D0400E1944D /* AuthRequestBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3390D1E51F054D0400E1944D /* AuthRequestBuilderProtocol.swift */; };
23+
3390D1E81F054D1E00E1944D /* Authorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3390D1E71F054D1E00E1944D /* Authorizer.swift */; };
2224
33A962771D89483600DA421E /* PusherConnectionDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33A962751D8943CA00DA421E /* PusherConnectionDelegateTests.swift */; };
2325
33BA541E1D90351B00CD853B /* NativePusherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BA541D1D90351B00CD853B /* NativePusherTests.swift */; };
2426
33BA54201D9035BD00CD853B /* PusherDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33BA541F1D9035BD00CD853B /* PusherDelegate.swift */; };
@@ -65,6 +67,8 @@
6567
3389F5711CAEDDF300563F49 /* PusherChannels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherChannels.swift; sourceTree = "<group>"; };
6668
3389F5751CAEDE2800563F49 /* PusherGlobalChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherGlobalChannel.swift; sourceTree = "<group>"; };
6769
3389F5791CAEDEC800563F49 /* PusherClientOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherClientOptions.swift; sourceTree = "<group>"; };
70+
3390D1E51F054D0400E1944D /* AuthRequestBuilderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthRequestBuilderProtocol.swift; sourceTree = "<group>"; };
71+
3390D1E71F054D1E00E1944D /* Authorizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authorizer.swift; sourceTree = "<group>"; };
6872
33A962751D8943CA00DA421E /* PusherConnectionDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PusherConnectionDelegateTests.swift; path = ../Tests/PusherConnectionDelegateTests.swift; sourceTree = "<group>"; };
6973
33BA541D1D90351B00CD853B /* NativePusherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NativePusherTests.swift; path = ../Tests/NativePusherTests.swift; sourceTree = "<group>"; };
7074
33BA541F1D9035BD00CD853B /* PusherDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PusherDelegate.swift; sourceTree = "<group>"; };
@@ -135,6 +139,8 @@
135139
3389F5711CAEDDF300563F49 /* PusherChannels.swift */,
136140
3341A3391D819FBC007191AD /* NativePusher.swift */,
137141
33C1FD6E1D81BFC300921AD7 /* ObjectiveC.swift */,
142+
3390D1E51F054D0400E1944D /* AuthRequestBuilderProtocol.swift */,
143+
3390D1E71F054D1E00E1944D /* Authorizer.swift */,
138144
33C0D2E71CB5C54C003FE13E /* Dependencies */,
139145
33831CD61A9CFFF200B124F1 /* PusherSwift.h */,
140146
33831C8C1A9CF61600B124F1 /* Supporting Files */,
@@ -329,6 +335,7 @@
329335
33BA54201D9035BD00CD853B /* PusherDelegate.swift in Sources */,
330336
3389F56A1CAEDD9100563F49 /* PusherWebsocketDelegate.swift in Sources */,
331337
330D7A6D1CAEDA750032FF2C /* PusherChannel.swift in Sources */,
338+
3390D1E81F054D1E00E1944D /* Authorizer.swift in Sources */,
332339
3389F5721CAEDDF300563F49 /* PusherChannels.swift in Sources */,
333340
3389F5761CAEDE2800563F49 /* PusherGlobalChannel.swift in Sources */,
334341
3389F57A1CAEDEC800563F49 /* PusherClientOptions.swift in Sources */,
@@ -339,6 +346,7 @@
339346
33C0D2D51CB5C1F2003FE13E /* CryptoSwiftHMACModule.swift in Sources */,
340347
33C0D2DB1CB5C364003FE13E /* Starscream.swift in Sources */,
341348
33C1FD6F1D81BFC300921AD7 /* ObjectiveC.swift in Sources */,
349+
3390D1E61F054D0400E1944D /* AuthRequestBuilderProtocol.swift in Sources */,
342350
330D7A6A1CAEDA6C0032FF2C /* PusherSwift.swift in Sources */,
343351
);
344352
runOnlyForDeploymentPostprocessing = 0;

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,15 @@ public enum AuthMethod {
117117
case endpoint(authEndpoint: String)
118118
case authRequestBuilder(authRequestBuilder: AuthRequestBuilderProtocol)
119119
case inline(secret: String)
120+
case authorizer(authorizer: Authorizer)
120121
case noMethod
121122
}
122123
```
123124

124125
- `endpoint(authEndpoint: String)` - the client will make a `POST` request to the endpoint you specify with the socket ID of the client and the channel name attempting to be subscribed to
125126
- `authRequestBuilder(authRequestBuilder: AuthRequestBuilderProtocol)` - you specify an object that conforms to the `AuthRequestBuilderProtocol` (defined below), which must generate an `NSURLRequest` object that will be used to make the auth request
126127
- `inline(secret: String)` - your app's secret so that authentication requests do not need to be made to your authentication endpoint and instead subscriptions can be authenticated directly inside the library (this is mainly desgined to be used for development)
128+
- `authorizer(authorizer: Authorizer)` - you specify an object that conforms to the `Authorizer` protocol which must be able to provide the appropriate auth information
127129
- `noMethod` - if you are only using public channels then you do not need to set an `authMethod` (this is the default value)
128130

129131
This is the `AuthRequestBuilderProtocol` definition:
@@ -137,6 +139,32 @@ public protocol AuthRequestBuilderProtocol {
137139
}
138140
```
139141

142+
This is the `Authorizer` protocol definition:
143+
144+
```swift
145+
public protocol Authorizer {
146+
func fetchAuthValue(socketID: String, channelName: String, completionHandler: (PusherAuth?) -> ())
147+
}
148+
```
149+
150+
where `PusherAuth` is defined as:
151+
152+
```swift
153+
public class PusherAuth: NSObject {
154+
public let auth: String
155+
public let channelData: String?
156+
157+
public init(auth: String, channelData: String? = nil) {
158+
self.auth = auth
159+
self.channelData = channelData
160+
}
161+
}
162+
```
163+
164+
Provided the authorization process succeeds you need to then call the supplied `completionHandler` with a `PusherAuth` object so that the subscription process can complete.
165+
166+
If for whatever reason your authorization process fails then you just need to call the `completionHandler` with `nil` as the only parameter.
167+
140168
Note that if you want to specify the cluster to which you want to connect then you use the `host` property as follows:
141169

142170
#### Swift
@@ -610,6 +638,24 @@ Note that both private and presence channels require the user to be authenticate
610638
We recommend that you use an authentication endpoint over including your app's secret in your app in the vast majority of use cases. If you are completely certain that there's no risk to you including your app's secret in your app, for example if your app is just for internal use at your company, then it can make things easier than setting up an authentication endpoint.
611639
612640
641+
### Subscribing with self-provided auth values
642+
643+
It is possible to subscribe to channels that require authentication by providing the auth information at the point of calling `subscribe` or `subscribeToPresenceChannel`. This is done as shown below:
644+
645+
#### Swift
646+
647+
```swift
648+
let pusherAuth = PusherAuth(auth: yourAuthString, channelData: yourOptionalChannelDataString)
649+
let chan = self.pusher.subscribe(channelName, auth: pusherAuth)
650+
```
651+
652+
This PusherAuth object can be initialised with just an auth (String) value if the subscription is to a private channel, or both an `auth (String)` and `channelData (String)` pair of values if the subscription is to a presence channel.
653+
654+
These `auth` and `channelData` values are the values that you received if the json object created by a call to pusher.authenticate(...) in one of our various server libraries.
655+
656+
Keep in mind that in order to generate a valid auth value for a subscription the `socketId` (i.e. the unique identifier for a web socket connection to the Pusher servers) must be present when the auth value is generated. As such, the likely flow for using this is something like this would involve checking for when the connection state becomes `connected` before trying to subscribe to any channels requiring authentication.
657+
658+
613659
## Binding to events
614660

615661
Events can be bound to at 2 levels; globally and per channel. When binding to an event you can choose to save the return value, which is a unique identifier for the event handler that gets created. The only reason to save this is if you're going to want to unbind from the event at a later point in time. There is an example of this below.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
3+
@objc public protocol AuthRequestBuilderProtocol {
4+
@available(*, deprecated: 4.0.2, message: "use requestFor(socketID: String, channelName: String) -> URLRequest? instead")
5+
@objc optional func requestFor(socketID: String, channel: PusherChannel) -> NSMutableURLRequest?
6+
7+
@objc optional func requestFor(socketID: String, channelName: String) -> URLRequest?
8+
}

Source/Authorizer.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
@objc public protocol Authorizer {
4+
@objc func fetchAuthValue(socketID: String, channelName: String, completionHandler: (PusherAuth?) -> ())
5+
}

Source/ObjectiveC.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ public extension AuthMethod {
116116
return OCAuthMethod(authRequestBuilder: authRequestBuilder)
117117
case let .inline(secret):
118118
return OCAuthMethod(secret: secret)
119+
case let .authorizer(authorizer):
120+
return OCAuthMethod(authorizer: authorizer)
119121
case .noMethod:
120-
return OCAuthMethod(type: 3)
122+
return OCAuthMethod(type: 4)
121123
}
122124
}
123125

@@ -126,7 +128,8 @@ public extension AuthMethod {
126128
case 0: return AuthMethod.endpoint(authEndpoint: source.authEndpoint!)
127129
case 1: return AuthMethod.authRequestBuilder(authRequestBuilder: source.authRequestBuilder!)
128130
case 2: return AuthMethod.inline(secret: source.secret!)
129-
case 3: return AuthMethod.noMethod
131+
case 3: return AuthMethod.authorizer(authorizer: source.authorizer!)
132+
case 4: return AuthMethod.noMethod
130133
default: return AuthMethod.noMethod
131134
}
132135
}
@@ -137,6 +140,7 @@ public extension AuthMethod {
137140
var secret: String? = nil
138141
var authEndpoint: String? = nil
139142
var authRequestBuilder: AuthRequestBuilderProtocol? = nil
143+
var authorizer: Authorizer? = nil
140144

141145
public init(type: Int) {
142146
self.type = type
@@ -156,4 +160,9 @@ public extension AuthMethod {
156160
self.type = 2
157161
self.secret = secret
158162
}
163+
164+
public init(authorizer: Authorizer) {
165+
self.type = 3
166+
self.authorizer = authorizer
167+
}
159168
}

Source/PusherClientOptions.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,10 @@ public enum PusherHost {
2020
}
2121
}
2222

23-
@objc public protocol AuthRequestBuilderProtocol {
24-
@available(*, deprecated: 4.0.2, message: "use requestFor(socketID: String, channelName: String) -> URLRequest? instead")
25-
@objc optional func requestFor(socketID: String, channel: PusherChannel) -> NSMutableURLRequest?
26-
27-
@objc optional func requestFor(socketID: String, channelName: String) -> URLRequest?
28-
}
29-
3023
public enum AuthMethod {
3124
case endpoint(authEndpoint: String)
3225
case authRequestBuilder(authRequestBuilder: AuthRequestBuilderProtocol)
26+
case authorizer(authorizer: Authorizer)
3327
case inline(secret: String)
3428
case noMethod
3529
}

Source/PusherConnection.swift

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,6 @@ open class PusherConnection: NSObject {
579579
let request = requestForAuthValue(from: authEndpoint, socketId: socketId, channelName: channel.name)
580580
sendAuthorisationRequest(request: request, channel: channel)
581581
return true
582-
583582
case .authRequestBuilder(authRequestBuilder: let builder):
584583
if let request = builder.requestFor?(socketID: socketId, channel: channel) {
585584
sendAuthorisationRequest(request: request as URLRequest, channel: channel)
@@ -597,6 +596,17 @@ open class PusherConnection: NSObject {
597596

598597
return false
599598
}
599+
case .authorizer(authorizer: let authorizer):
600+
authorizer.fetchAuthValue(socketID: socketId, channelName: channel.name) { authInfo in
601+
guard let authInfo = authInfo else {
602+
print("Auth info passed to authorizer completionHandler was nil so channel subscription failed")
603+
return
604+
}
605+
606+
handleAuthInfo(authString: authInfo.auth, channelData: authInfo.channelData, channel: channel)
607+
}
608+
609+
return true
600610
case .inline(secret: let secret):
601611
var msg = ""
602612
var channelData = ""
@@ -724,28 +734,43 @@ open class PusherConnection: NSObject {
724734
}
725735

726736
/**
727-
Handle authentication request response and call appropriate handle function
737+
Handle authorizer request response and call appropriate handle function
728738

729739
- parameter json: The auth response as a dictionary
730-
- parameter channel: The PusherChannel to authenticate subsciption for
740+
- parameter channel: The PusherChannel to authorize subsciption for
731741
*/
732742
fileprivate func handleAuthResponse(
733743
json: [String : AnyObject],
734744
channel: PusherChannel) {
735745
if let auth = json["auth"] as? String {
736-
if let channelData = json["channel_data"] as? String {
737-
handlePresenceChannelAuth(authValue: auth, channel: channel, channelData: channelData)
738-
} else {
739-
handlePrivateChannelAuth(authValue: auth, channel: channel)
740-
}
746+
handleAuthInfo(
747+
authString: auth,
748+
channelData: json["channel_data"] as? String,
749+
channel: channel
750+
)
741751
}
742752
}
743753

754+
/**
755+
Handle authorizer info and call appropriate handle function
756+
757+
- parameter authString: The auth response as a dictionary
758+
- parameter channelData: The channelData to send along with the auth request
759+
- parameter channel: The PusherChannel to authorize the subsciption for
760+
*/
761+
fileprivate func handleAuthInfo(authString: String, channelData: String?, channel: PusherChannel) {
762+
if let channelData = channelData {
763+
handlePresenceChannelAuth(authValue: authString, channel: channel, channelData: channelData)
764+
} else {
765+
handlePrivateChannelAuth(authValue: authString, channel: channel)
766+
}
767+
}
768+
744769
/**
745770
Handle presence channel auth response and send subscribe message to Pusher API
746771

747772
- parameter auth: The auth string
748-
- parameter channel: The PusherChannel to authenticate subsciption for
773+
- parameter channel: The PusherChannel to authorize subsciption for
749774
- parameter channelData: The channelData to send along with the auth request
750775
*/
751776
fileprivate func handlePresenceChannelAuth(

Tests/AuthenticationTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,68 @@ class AuthenticationTests: XCTestCase {
246246

247247
waitForExpectations(timeout: 0.5)
248248
}
249+
250+
func testAuthorizationUsingSomethingConformingToTheAuthorizerProtocol() {
251+
252+
class SomeAuthorizer: Authorizer {
253+
func fetchAuthValue(socketID: String, channelName: String, completionHandler: (PusherAuth?) -> ()) {
254+
completionHandler(PusherAuth(auth: "testKey123:authorizerblah123"))
255+
}
256+
}
257+
258+
let ex = expectation(description: "the channel should be subscribed to successfully")
259+
let channelName = "private-test-channel-authorizer"
260+
261+
let dummyDelegate = DummyDelegate()
262+
dummyDelegate.ex = ex
263+
dummyDelegate.testingChannelName = channelName
264+
265+
let options = PusherClientOptions(
266+
authMethod: AuthMethod.authorizer(authorizer: SomeAuthorizer())
267+
)
268+
pusher = Pusher(key: "testKey123", options: options)
269+
pusher.delegate = dummyDelegate
270+
socket.delegate = pusher.connection
271+
pusher.connection.socket = socket
272+
273+
let chan = pusher.subscribe(channelName)
274+
XCTAssertFalse(chan.subscribed, "the channel should not be subscribed")
275+
pusher.connect()
276+
277+
waitForExpectations(timeout: 0.5)
278+
}
279+
280+
func testAuthorizationOfPresenceChannelSubscriptionUsingSomethingConformingToTheAuthorizerProtocol() {
281+
282+
class SomeAuthorizer: Authorizer {
283+
func fetchAuthValue(socketID: String, channelName: String, completionHandler: (PusherAuth?) -> ()) {
284+
completionHandler(PusherAuth(
285+
auth: "testKey123:authorizerblah1234",
286+
channelData: "{\"user_id\":\"777\", \"user_info\":{\"twitter\":\"hamchapman\"}}"
287+
))
288+
}
289+
}
290+
291+
let ex = expectation(description: "the channel should be subscribed to successfully")
292+
let channelName = "presence-test-channel-authorizer"
293+
294+
let dummyDelegate = DummyDelegate()
295+
dummyDelegate.ex = ex
296+
dummyDelegate.testingChannelName = channelName
297+
298+
let options = PusherClientOptions(
299+
authMethod: AuthMethod.authorizer(authorizer: SomeAuthorizer())
300+
)
301+
pusher = Pusher(key: "testKey123", options: options)
302+
pusher.delegate = dummyDelegate
303+
socket.delegate = pusher.connection
304+
pusher.connection.socket = socket
305+
306+
let chan = pusher.subscribe(channelName)
307+
XCTAssertFalse(chan.subscribed, "the channel should not be subscribed")
308+
pusher.connect()
309+
310+
waitForExpectations(timeout: 0.5)
311+
}
312+
249313
}

Tests/Mocks.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ open class MockWebSocket: WebSocket {
168168
self.delegate?.websocketDidReceiveMessage(socket: self, text: "{\"event\":\"pusher_internal:subscription_succeeded\",\"data\":\"{\\\"presence\\\":{\\\"count\\\":1,\\\"ids\\\":[\\\"123\\\"],\\\"hash\\\":{\\\"123\\\":{\\\"friends\\\":0}}}}\",\"channel\":\"presence-channel\"}")
169169
}
170170
)
171+
} else if stringContainsElements(string, elements: ["pusher:subscribe", "testKey123:authorizerblah123", "private-test-channel-authorizer"]) {
172+
let _ = stubber.stub(
173+
functionName: "writeString",
174+
args: [string],
175+
functionToCall: {
176+
self.delegate?.websocketDidReceiveMessage(socket: self, text: "{\"event\":\"pusher_internal:subscription_succeeded\",\"channel\":\"private-test-channel-authorizer\",\"data\":\"{}\"}")
177+
}
178+
)
179+
} else if stringContainsElements(string, elements: ["pusher:subscribe", "testKey123:authorizerblah1234", "presence-test-channel-authorizer"]) {
180+
let _ = stubber.stub(
181+
functionName: "writeString",
182+
args: [string],
183+
functionToCall: {
184+
self.delegate?.websocketDidReceiveMessage(socket: self, text: "{\"event\":\"pusher_internal:subscription_succeeded\",\"data\":\"{\\\"presence\\\":{\\\"count\\\":1,\\\"ids\\\":[\\\"777\\\"],\\\"hash\\\":{\\\"777\\\":{\\\"twitter\\\":\\\"hamchapman\\\"}}}}\",\"channel\":\"presence-test-channel-authorizer\"}")
185+
}
186+
)
171187
} else {
172188
print("No match in write(string: ...) mock for string: \(string)")
173189
}

0 commit comments

Comments
 (0)