Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jwt tests #714

Merged
merged 23 commits into from
Jun 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Source/ARTAuth.m
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ - (void)handleAuthUrlResponse:(NSHTTPURLResponse *)response withData:(NSData *)d
callback(tokenDetails, nil);
}
}
else if ([response.MIMEType isEqualToString:@"text/plain"]) {
else if ([response.MIMEType isEqualToString:@"text/plain"] || [response.MIMEType isEqualToString:@"application/jwt"]) {
NSString *token = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if ([token isEqualToString:@""]) {
callback(nil, [NSError errorWithDomain:ARTAblyErrorDomain code:NSURLErrorCancelled userInfo:@{NSLocalizedDescriptionKey:@"authUrl: token is empty"}]);
Expand Down
2 changes: 1 addition & 1 deletion Source/ARTRealtime.m
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ - (void)onError:(ARTProtocolMessage *)message {
return;
}
[self.connection setId:nil];
[self transition:ARTRealtimeFailed withErrorInfo:error];
[self transition:ARTRealtimeFailed withErrorInfo:message.error];
}
} ART_TRY_OR_MOVE_TO_FAILED_END
}
Expand Down
327 changes: 326 additions & 1 deletion Spec/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class Auth : QuickSpec {
guard let error = error else {
fail("Error is nil"); done(); return
}
expect(UInt(error.code)).to(equal(ARTState.requestTokenFailed.rawValue))
expect(error.code).to(equal(40142))
expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.failed))
done()
}
Expand Down Expand Up @@ -3619,5 +3619,330 @@ class Auth : QuickSpec {
}
}
}

describe("JWT and realtime") {
let channelName = "test_JWT"
let messageName = "message_JWT"

context("client initialized with a JWT token in ClientOptions") {
let options = AblyTests.clientOptions()

context("with valid credentials") {
options.token = getJWTToken()
let client = AblyTests.newRealtime(options)
defer { client.dispose(); client.close() }

it("pulls stats successfully") {
waitUntil(timeout: testTimeout) { done in
client.stats { stats, error in
expect(error).to(beNil())
done()
}
}
}
}

context("with invalid credentials") {
options.token = getJWTToken(invalid: true)
options.autoConnect = false
let client = AblyTests.newRealtime(options)
defer { client.dispose(); client.close() }

it("fails to connect with reason 'invalid signature'") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.failed) { stateChange in
expect(stateChange!.reason!.code).to(equal(40144))
expect(stateChange!.reason!.description).to(contain("invalid signature"))
done()
}
client.connect()
}
}
}
}

// RSA8g RSA8c
context("when using authUrl") {
let options = AblyTests.clientOptions()
let keys = getKeys()
options.authUrl = NSURL(string: echoServerAddress)! as URL

context("with valid credentials") {
options.authParams = [URLQueryItem]() as [URLQueryItem]?
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]) as URLQueryItem)
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("fetches a channels and posts a message") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.connected, callback: { _ in
let channel = client.channels.get(channelName)
channel.publish(messageName, data: nil, callback: { error in
expect(error).to(beNil())
done()
})
})
client.connect()
}
}
}

context("with wrong credentials") {
options.authParams = [URLQueryItem]() as [URLQueryItem]?
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "keySecret", value: "INVALID") as URLQueryItem)
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("fails to connect with reason 'invalid signature'") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.failed) { stateChange in
expect(stateChange!.reason!.code).to(equal(40144))
expect(stateChange!.reason!.description).to(contain("invalid signature"))
done()
}
client.connect()
}
}
}

context("when token expires") {
let tokenDuration = 5.0
options.authParams = [URLQueryItem]() as [URLQueryItem]?
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "expiresIn", value: String(UInt(tokenDuration))) as URLQueryItem)
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it ("receives a 40142 error from the server") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.connected) { stateChange in
client.connection.once(.disconnected) { stateChange in
expect(stateChange!.reason!.code).to(equal(40142))
expect(stateChange!.reason!.description).to(contain("Key/token status changed (expire)"))
done()
}
}
client.connect()
}
}
}

// RTC8a4
context("when the server sends and AUTH protocol message") {
it("client reauths correctly without going through a disconnection") {
// The server sends an AUTH protocol message 30 seconds before a token expires
// We create a token that lasts 35 seconds, so there's room to receive the AUTH message
let tokenDuration = 35.0
options.authParams = [URLQueryItem]() as [URLQueryItem]?
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "expiresIn", value: String(UInt(tokenDuration))) as URLQueryItem)
options.autoConnect = false // Prevent auto connection so we can set the transport proxy
let client = ARTRealtime(options: options)
client.setTransport(TestProxyTransport.self)
defer { client.dispose(); client.close() }

waitUntil(timeout: testTimeout) { done in
client.connection.once(.connected) { stateChange in
let originalToken = client.auth.tokenDetails?.token
let transport = client.transport as! TestProxyTransport

client.connection.once(.update) { stateChange in
expect(transport.protocolMessagesReceived.filter({ $0.action == .auth })).to(haveCount(1))
expect(originalToken).toNot(equal(client.auth.tokenDetails?.token))
done()
}
}
client.connect()
}
}
}
}

// RSA8g
context("when using authCallback") {
let options = AblyTests.clientOptions()

context("with valid credentials") {
options.authCallback = { tokenParams, completion in
let token = ARTTokenDetails(token: getJWTToken()!)
completion(token, nil)
}
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("pulls stats successfully") {
waitUntil(timeout: testTimeout) { done in
client.stats { stats, error in
expect(error).to(beNil())
done()
}
}
}
}

context("with invalid credentials") {
options.authCallback = { tokenParams, completion in
let token = ARTTokenDetails(token: getJWTToken(invalid: true)!)
completion(token, nil)
}
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("fails to connect") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.failed) { stateChange in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we had agreed that https://docs.ably.io/client-lib-development-guide/features/#RSA4b stated that the connection should not become failed, but instead should become disconnected and retry. I believe @paddybyers updated the spec after chatting with you about this at some point, although oddly I can't find a matching commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this as a TODO above

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattheworiordan RSA4b says that a REST request fails if a token error is returned after a retry, which is correct, and is unrelated to this test.

https://docs.ably.io/client-lib-development-guide/features/#RTN14b relates to token failures on connection creation - it says that a connection should not fail in that case, but become disconnected. This test is therefore wrong and, because it passes, there is also a bug in the library (which will have existed before these JWT changes). @funkyboy pls raise a separate issue for that (and find the other existing, non-JWT, invalid test).

The discussion you remembered related was https://github.com/ably/docs/issues/429, relating to RTN15h, and that's to do with failures to reconnect (ie not initial connection failures). I'm guessing that there's going to be an issue for iOS there as well. I will do a docs PR for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paddybyers @mattheworiordan The non-JWT corresponding test is https://github.com/ably/ably-ios/blob/develop/Spec/Auth.swift#L305 and again it tests for FAILED.

Issue raised here: #730

Copy link
Member

@paddybyers paddybyers Jun 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do a docs PR for that.

ably/docs#439. @funkyboy can you check that behaviour please in this library and see if complies with the DISCONNECTED or FAILED behaviour?

Copy link
Contributor Author

@funkyboy funkyboy Jun 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paddybyers This seems to test the new RTN15h1 that you wrote.
These all riff around the same "token expired/wrong" conditions but none seems to tests for the "single attempt to renew the token" clause.

Should I open a new issue about this?

cc @mattheworiordan

UPDATE: this seems close to test this https://github.com/ably/docs/pull/439/files#diff-11900b395df266bc2bbfc1d11bdfc7a0R390 but it's not checking for the connection state :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattheworiordan RSA4b says that a REST request fails if a token error is returned after a retry, which is correct, and is unrelated to this test.

Oops, I should have scrolled up to see the context of the test I found. That explains why I could not find the record of the change we made in the spec!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paddybyers

ably/docs#439. @funkyboy can you check that behaviour please in this library and see if complies with the DISCONNECTED or FAILED behaviour?

Opened issue here #731

expect(stateChange!.reason!.code).to(equal(40144))
expect(stateChange!.reason!.description).to(contain("invalid signature"))
done()
}
client.connect()
}
}
}
}

context("when token expires and has a means to renew") {

it("reconnects using authCallback and obtains a new token") {
let tokenDuration = 3.0
let options = AblyTests.clientOptions()
options.useTokenAuth = true
options.autoConnect = false
options.authCallback = { tokenParams, completion in
let token = ARTTokenDetails(token: getJWTToken(expiresIn: Int(tokenDuration))!)
completion(token, nil)
}
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }
var originalToken = ""
var originalConnectionID = ""
waitUntil(timeout: testTimeout) { done in
client.connection.once(.connected) { _ in
originalToken = client.auth.tokenDetails!.token
originalConnectionID = client.connection.id!

client.connection.once(.disconnected) { stateChange in
expect(stateChange!.reason!.code).to(equal(40142))

client.connection.once(.connected) { _ in
expect(client.connection.id).to(equal(originalConnectionID))
expect(client.auth.tokenDetails!.token).toNot(equal(originalToken))
done()
}
}
}
client.connect()
}
}
}

context("when the token request includes a clientId") {
let clientId = "JWTClientId"
let options = AblyTests.clientOptions()
options.tokenDetails = ARTTokenDetails(token: getJWTToken(clientId: clientId)!)
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("the clientId is the same specified in the JWT token request") {
waitUntil(timeout: testTimeout) { done in
client.connection.once(.connected) { _ in
expect(client.auth.clientId).to(equal(clientId))
done()
}
client.connect()
}
}
}

context("when the token request includes subscribe-only capabilities") {
let capability = "{\"\(channelName)\":[\"subscribe\"]}"
let options = AblyTests.clientOptions()
options.tokenDetails = ARTTokenDetails(token: getJWTToken(capability: capability)!)
let client = ARTRealtime(options: options)
defer { client.dispose(); client.close() }

it("fails to publish to a channel with subscribe-only capability") {
waitUntil(timeout: testTimeout) { done in
client.channels.get(channelName).publish(messageName, data: nil, callback: { error in
expect(error?.code).to(equal(90001))
expect(error?.message).to(contain("channel operation failed"))
done()
})
}
}
}
}

// RSC1 RSC1a RSC1c RSA3d
describe("JWT and rest") {
let options = AblyTests.clientOptions()

context("when the JWT token embeds an Ably token") {
options.tokenDetails = ARTTokenDetails(token: getJWTToken(jwtType: "embedded")!)
let client = ARTRest(options: options)

it ("pulls stats successfully") {
waitUntil(timeout: testTimeout) { done in
client.stats { stats, error in
expect(error).to(beNil())
done()
}
}
}
}

context("when the JWT token embeds an Ably token and it is requested as encrypted") {
options.tokenDetails = ARTTokenDetails(token: getJWTToken(jwtType: "embedded", encrypted: 1)!)
let client = ARTRest(options: options)

it ("pulls stats successfully") {
waitUntil(timeout: testTimeout) { done in
client.stats { stats, error in
expect(error).to(beNil())
done()
}
}
}
}

// RSA4f, RSA8c
context("when the JWT token is returned with application/jwt content type") {
let options = AblyTests.clientOptions()
let keys = getKeys()
options.authUrl = NSURL(string: echoServerAddress)! as URL
options.authParams = [URLQueryItem]() as [URLQueryItem]?
options.authParams?.append(URLQueryItem(name: "keyName", value: keys["keyName"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "keySecret", value: keys["keySecret"]) as URLQueryItem)
options.authParams?.append(URLQueryItem(name: "returnType", value: "jwt") as URLQueryItem)
let client = ARTRest(options: options)

it("the client successfully connects and pulls stats") {
waitUntil(timeout: testTimeout) { done in
client.stats { stats, error in
expect(error).to(beNil())
done()
}
}
}

it("the client can request a new token to initilize another client that connects and pulls stats") {
waitUntil(timeout: testTimeout) { done in
client.auth.requestToken(nil, with: nil, callback: { tokenDetails, error in
let newClientOptions = AblyTests.clientOptions()
newClientOptions.token = tokenDetails!.token
let newClient = ARTRest(options: newClientOptions)
newClient.stats { stats, error in
expect(error).to(beNil())
done()
}
})
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion Spec/RealtimeClientConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2011,7 +2011,7 @@ class RealtimeClientConnection: QuickSpec {
guard let errorInfo = errorInfo else {
fail("ErrorInfo is nil"); done(); return
}
expect(UInt(errorInfo.code)).to(equal(ARTState.requestTokenFailed.rawValue))
expect(errorInfo.code).to(equal(40142))
done()
default:
break
Expand Down
Loading