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

MBL-1157: Create PKCE code for code verifier and code challenge #1921

Merged
merged 3 commits into from Jan 31, 2024

Conversation

amy-at-kickstarter
Copy link
Contributor

📲 What

Added a utility for PKCE verification.

🤔 Why

PKCE is part of the native apps flow for oAuth - we create a secret (the code verifier) and a challenge (a hashed version) which gets sent to the server. This is used to verify that every step of the oAuth flow is legit. Later in the flow, the client has to prove that it was the one that generated the hash by providing the original secret.

For more, see: https://www.oauth.com/oauth2-servers/mobile-and-native-apps/authorization/

🛠 How

I used this example code, as well as the documentation above.

⏰ TODO

I'd like to add more tests to this, which I'll coordinate with Isa on the Android side. These tests are just to make sure the code is running.

KsApi/PKCE.swift Outdated
/// PKCE stands for Proof Key for Code Exchange.
/// The code verifier, and its associated challenge, are used to ensure that an oAuth request is valid.
/// See this documentation for more details: https://www.oauth.com/oauth2-servers/mobile-and-native-apps/authorization/
public struct PKCE {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This doesn't need to be a struct; I'm just doing it for the sake of namespace clarity. These methods are called like PKCE.createCodeVerifier(ofLength:).

KsApi/PKCE.swift Outdated
}

guard let unwrappedData = hashData else {
throw ErrorType.UnableToCreateCodeChallenge
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somewhat annoyingly - there are a few cases in this code where I need to unwrap a nil coming from a system library. In the interest of making this bulletproof, I'm throwing errors if those unwraps fail - but I never expect that to happen, pragmatically.

If any part of PKCE fails, I think the correct solution is to abort the login process, and just let the user continue in a logged-out state.

@testable import KsApi
import XCTest

final class PKCETest: XCTestCase {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some rough tests - we'll coordinate more examples later, to make sure iOS + Android have the same behavior.

KsApi/PKCE.swift Outdated
var codeVerifier = ""

for _ in 0..<length {
guard let character = validCharacters.randomElement() else {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Arkariang So this is something I wanted to verify - randomElement() uses the system random number generator. Is that going to be good enough for PKCE? The documentation says "cryptographically random" and it's not clear if it needs a more random kind of random than the system provides.

Copy link
Contributor

Choose a reason for hiding this comment

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

I hesitate to rely on the default random generator for crypto - all my crypto classes in college taught "never implement crypto yourself if you can avoid it". However, I did a little digging and it looks fairly safe.

Reasoning: https://developer.apple.com/documentation/swift/systemrandomnumbergenerator claims that the system default random number generator is cryptographically secure whenever possible. https://developer.apple.com/documentation/security/randomization_services, which is cryptographically secure, uses the system default as its source of randomness (unsure what it does if the system default isn't good enough). .randomElement uses the system default as well.

The main problem with this approach is that I'm not sure what happens if the system default isn't secure enough (which could happen if there isn't a lot of entropy to draw from - ex new computers booted up at the same time will probably have the same seed and get the same "random" numbers). It's possible that Apple's cryptographically secure random bytes method would throw an error instead in this case, and that that's what makes it actually cryptographically random. However, I don't know enough about PKCE to know if this would actually be a problem we need to handle; I expect our real users would have cryptographically random verifiers.

I think I'd recommend leaving this as-is for now and going to security office hours next week. Or looking for maybe 3rd party solutions for getting guaranteed cryptographic randomness. (Or using https://developer.apple.com/documentation/security/1399291-secrandomcopybytes, but then we can't specify what kinds of bytes we're okay with, as far as I can tell.)

Choose a reason for hiding this comment

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

I think it is refered to use some kind of secure hash algorithm, in my case for Android I'll use SHA-256

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it is refered to use some kind of secure hash algorithm, in my case for Android I'll use SHA-256

Aah that's what I'm using for the code challenge. I saw in your example code that the code verifier uses SecureRandom.nextBytes(), and I was hoping to find an iOS equivalent.

I think I'd recommend leaving this as-is for now and going to security office hours next week.

Aha, thanks for digging into the system randomizer, that's super interesting. (And for finding SecRandomCopyBytes. I knew that existed somewhere but couldn't remember what it was called, for the life of me.) I think security office hours is a great idea - I'd love to get a more expert opinion on this.

KsApi/PKCE.swift Outdated
var codeVerifier = ""

for _ in 0..<length {
guard let character = validCharacters.randomElement() else {
Copy link
Contributor

Choose a reason for hiding this comment

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

I hesitate to rely on the default random generator for crypto - all my crypto classes in college taught "never implement crypto yourself if you can avoid it". However, I did a little digging and it looks fairly safe.

Reasoning: https://developer.apple.com/documentation/swift/systemrandomnumbergenerator claims that the system default random number generator is cryptographically secure whenever possible. https://developer.apple.com/documentation/security/randomization_services, which is cryptographically secure, uses the system default as its source of randomness (unsure what it does if the system default isn't good enough). .randomElement uses the system default as well.

The main problem with this approach is that I'm not sure what happens if the system default isn't secure enough (which could happen if there isn't a lot of entropy to draw from - ex new computers booted up at the same time will probably have the same seed and get the same "random" numbers). It's possible that Apple's cryptographically secure random bytes method would throw an error instead in this case, and that that's what makes it actually cryptographically random. However, I don't know enough about PKCE to know if this would actually be a problem we need to handle; I expect our real users would have cryptographically random verifiers.

I think I'd recommend leaving this as-is for now and going to security office hours next week. Or looking for maybe 3rd party solutions for getting guaranteed cryptographic randomness. (Or using https://developer.apple.com/documentation/security/1399291-secrandomcopybytes, but then we can't specify what kinds of bytes we're okay with, as far as I can tell.)

case UnexpectedRuntimeError
}

public extension Data {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

KsApi/PKCE.swift Outdated
throw PKCEError.UnexpectedRuntimeError
}

_ = SecRandomCopyBytes(kSecRandomDefault, numBytes, address)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ifosli Thanks for reminding me what on earth this method was called - I was looking for it and couldn't find it 😅

do {
var buffer = Data(count: length)
try buffer.fillWithRandomSecureBytes()
return buffer.base64URLEncodedStringWithNoPadding()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is how the RFC for OAuth suggests doing it - you create a shorter, cryptographically random buffer, and use base64 URL encoding to turn it into the valid character set.

}

guard let unwrappedData = hashData else {
throw PKCEError.UnexpectedRuntimeError
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, I could make these methods return a nullable Data? and handle it further down. Preferences?

Copy link
Contributor

@ifosli ifosli left a comment

Choose a reason for hiding this comment

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

Apart from checking the return value of the secure bytes function, looks good to me!

KsApi/PKCE.swift Outdated
throw PKCEError.UnexpectedRuntimeError
}

_ = SecRandomCopyBytes(kSecRandomDefault, numBytes, address)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think we should confirm that the return type is errSecSuccess (https://developer.apple.com/documentation/security/1399291-secrandomcopybytes#discussion) and throw an error if not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good one; fixed!

let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let lowercase = "abcdefghijklmnopqrstuvwxyz"
let numbers = "0123456789"
let specials = "-._~"
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, how would you get . or ~ in the verifier?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question - the RFC mentioned that these were valid, but you're right, they're not part of base64url 🤷‍♀️

@amy-at-kickstarter amy-at-kickstarter merged commit dd545c0 into main Jan 31, 2024
5 checks passed
@amy-at-kickstarter amy-at-kickstarter deleted the feat/adyer/MBL-1157/pkce branch January 31, 2024 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants