-
Notifications
You must be signed in to change notification settings - Fork 12
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
Refactor Cookie generation-&-parsing #152
Conversation
c11c67f
to
8592c75
Compare
65ca26c
to
c081b7e
Compare
8592c75
to
a0f4f81
Compare
350d62d
to
4b540ec
Compare
4b540ec
to
6c4c583
Compare
0630bdd
to
e1fcd7a
Compare
6c4c583
to
9f20fc6
Compare
e1fcd7a
to
d565dd6
Compare
9f20fc6
to
ab80840
Compare
d565dd6
to
3cd167f
Compare
ab80840
to
ec6ca77
Compare
3cd167f
to
af01b6b
Compare
ec6ca77
to
efcee19
Compare
af01b6b
to
c483501
Compare
efcee19
to
c06d3fd
Compare
* A representation of the underlying binary data (both payload & signature) in a Panda cookie. | ||
* | ||
* If an instance has been parsed from a cookie's text value, the existence of the instance | ||
* *does not* imply that the signature has been verified. It only means that the cookie text was |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this abstraction seems very useful to me, though I wonder if there's a way to make it more obvious from the name of the case class that having a CookiePayload
doesn't imply that its signature's been verified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe UnverifiedCookiePayload
? I think bringing in Unverified
is a bit tricky though, because CookiePayload
transforms both ways and so changing its name also affects what the code looks like when we generate a cookie:
Line 53 in 41c7baf
CookiePayload.generateForPayloadText(serializeAuthenticatedUser(authUser), prvKey).asCookieText |
...given that at that point we're literally signing it with a private key, it wouldn't feel quite right to me to be referring to it as an Unverified CookiePayload...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes I see your point.. probably it would require having two different data types in that case, which seems like it'd be OTT at this point for a fairly theoretical issue 👍
user = User(firstName, lastName, email, data.get("avatarUrl")), | ||
authenticatingSystem = system, | ||
authenticatedIn = Set(authedIn.split(",").toSeq :_*), | ||
expires = expires.toLong, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return type of deserializeAuthenticatedUser
has been changed to Option[AuthenticatedUser]
in this branch. Is the intention that all foreseeable errors should therefore lead to a None
, rather than throwing? If so, do we need to change how we're handling the .toLong
call, given that it can throw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! Addressed in f40aa8d.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops - ah, but it fails because we have to compile under Scala 2.12:
[error] /home/runner/work/pan-domain-authentication/pan-domain-authentication/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CookieUtils.scala:41:48: value toLongOption is not a member of String
[error] expires <- data.get("expires").flatMap(_.toLongOption)
[error] ^
[error] /home/runner/work/pan-domain-authentication/pan-domain-authentication/pan-domain-auth-verification/src/main/scala/com/gu/pandomainauth/service/CookieUtils.scala:42:56: value toBooleanOption is not a member of String
[error] multiFactor <- data.get("multifactor").flatMap(_.toBooleanOption)
[error] ^
[error] two errors found
...I'll make something Scala 2.12 compliant...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...I'll make something Scala 2.12 compliant...
Done with 998db1f!
case _ => throw new CookieParseException | ||
} | ||
} | ||
// We would quite like to know, if a user is using an old (but accepted) key, *who* that user is- or to put it another |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I follow how this code does what the comment says..
From what I can see it'll either return an AuthenticatedUser
, or a SignatureNotValid
(or one of the other failure types), but none of these include the key that the user was using.. Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was a note to myself I should have removed - done in 41c7baf!
import org.scalatest.matchers.should.Matchers | ||
import TestKeys._ | ||
|
||
class CookiePayloadTest extends AnyFreeSpec with Matchers with OptionValues { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks to me like these cases provide good coverage 👍
c06d3fd
to
a8e3840
Compare
@bryophyta pointed out that this comment was describing something that the code _didn't do_ - it was a note to myself I should have deleted! https://github.com/guardian/pan-domain-authentication/pull/152/files#r1711588388
val testDependencies = Seq("org.scalatest" %% "scalatest" % "3.2.0" % "test") | ||
val testDependencies = Seq("org.scalatest" %% "scalatest" % "3.2.19" % Test) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need ScalaTest to be updated to at least v3.2.3, because we want to use EitherValues
right-biased .value
in tests, which was only implemented with scalatest/scalatest#1895. ScalaTest v3.2.19 is currently the latest release.
lazy val CookieRegEx = "^^([\\w\\W]*)\\.([\\w\\W]*)$".r | ||
|
||
def parseCookieData(cookieString: String, pubKey: PublicKey): AuthenticatedUser = { | ||
|
||
cookieString match { | ||
case CookieRegEx(data, sig) => | ||
try { | ||
if (Crypto.verifySignature(Base64.decodeBase64(data.getBytes("UTF-8")), Base64.decodeBase64(sig.getBytes("UTF-8")), pubKey)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This parsing of cookie-text (to extract the binary data of the payload & signature) has now moved to CookiePayload.parse()
.
case _ => throw new CookieParseException | ||
} | ||
} | ||
// We would quite like to know, if a user is using an old (but accepted) key, *who* that user is- or to put it another |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was a note to myself I should have removed - done in 41c7baf!
@bryophyta pointed out that although the `deserializeAuthenticatedUser()` method was now returning `Option[AuthenticatedUser]` (with a `None` return-value indicating conditions that would formerly have thrown an exception), there were still some cases where an exception would be thrown (parsing numbers and booleans): #152 (comment)
Revert this when #149 is fixed!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks good to me 👍
This upgrades Panda from v5 to v7, allowing us to use key rotation as introduced with guardian/pan-domain-authentication#150. As login.gutools.co.uk is pretty special user of Panda the upgrade is slightly more involved than other upgrades (eg guardian/atom-workshop#361): * Panda v6: * guardian/pan-domain-authentication#152 `CookieUtils.generateCookieData()` now communicates errors with `CookieResult` values containing `CookieIntegrityFailure`, rather than exceptions. * Panda v7: * guardian/pan-domain-authentication#150 means that code shouldn't directly reference private or public keys anymore (eg do not reference `settings.signingKeyPair`). Instead, use `settings.signingAndVerification` or `publicSettings.verification`. Note also that `publicSettings.publicKey` was previously optional, and `publicSettings.verification` is not.
This upgrades Panda from v5 to v7, allowing us to use key rotation as introduced with guardian/pan-domain-authentication#150. As login.gutools.co.uk is pretty special user of Panda the upgrade is slightly more involved than other upgrades (eg guardian/atom-workshop#361): * Panda v6: * guardian/pan-domain-authentication#152 `CookieUtils.generateCookieData()` now communicates errors with `CookieResult` values containing `CookieIntegrityFailure`, rather than exceptions. * Panda v7: * guardian/pan-domain-authentication#150 means that code shouldn't directly reference private or public keys anymore (eg do not reference `settings.signingKeyPair`). Instead, use `settings.signingAndVerification` or `publicSettings.verification`. Note also that `publicSettings.publicKey` was previously optional, and `publicSettings.verification` is not.
This upgrades Panda from v3 to v7, allowing us to use key rotation as introduced with guardian/pan-domain-authentication#150. ### Necessary code changes * Panda v5 * guardian/pan-domain-authentication#147 removed the old `PublicKey` & `PrivateKey` classes in our `com.gu.pandomainauth` package, in favour of using the existing `java.security` classes. To create instances of those classes, we can use the `SettingsReader.{privateKeyFor, publicKeyFor}` methods. * Panda v6: * guardian/pan-domain-authentication#152 means the `CookieUtils.generateCookieData()` method now communicates errors with `CookieResult` values containing `CookieIntegrityFailure`, rather than exceptions. * Panda v7: * guardian/pan-domain-authentication#150 means that code shouldn't directly reference private or public keys anymore (eg do not reference `settings.signingKeyPair`). Instead, use `settings.signingAndVerification` or `publicSettings.verification`. Note also that `publicSettings.publicKey` was previously optional, and `publicSettings.verification` is not.
This upgrades Panda from v3 to v7, allowing us to use key rotation as introduced with guardian/pan-domain-authentication#150. ### Necessary code changes * Panda v5 * guardian/pan-domain-authentication#147 removed the old `PublicKey` & `PrivateKey` classes in our `com.gu.pandomainauth` package, in favour of using the existing `java.security` classes. To create instances of those classes, we can use the `SettingsReader.{privateKeyFor, publicKeyFor}` methods. * Panda v6: * guardian/pan-domain-authentication#152 means the `CookieUtils.generateCookieData()` method now communicates errors with `CookieResult` values containing `CookieIntegrityFailure`, rather than exceptions. * Panda v7: * guardian/pan-domain-authentication#150 means that code shouldn't directly reference private or public keys anymore (eg do not reference `settings.signingKeyPair`). Instead, use `settings.signingAndVerification` or `publicSettings.verification`. Note also that `publicSettings.publicKey` was previously optional, and `publicSettings.verification` is not.
This is a precursor to PR #150 ("Support accepting multiple public keys"). It helps with that PR because we're going to be changing the cookie processing code to validate against a range of possible acceptable keys, and if a failure occurs in the reading of a cookie, we want to be clear about whether that happens to be due to the particular key we're attempting to validate against, or whether the cookie text itself is malformatted.
CookiePayload
: an optimal representation of cookie data for checking signature-validity against multiple public keys. TheCookiePayload
class represents the binary data caught after parsing-and-base64-decoding the cookie text value, ready for signature verification.CookieResult
: rather than throw exceptions, communicateCookieIntegrityFailure
s with return values ofCookieResult
.