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

Refactor Cookie generation-&-parsing #152

Merged
merged 4 commits into from
Sep 4, 2024

Conversation

rtyley
Copy link
Member

@rtyley rtyley commented Aug 7, 2024

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.

  • New class CookiePayload: an optimal representation of cookie data for checking signature-validity against multiple public keys. The CookiePayload class represents the binary data caught after parsing-and-base64-decoding the cookie text value, ready for signature verification.
  • New type alias CookieResult: rather than throw exceptions, communicate CookieIntegrityFailures with return values of CookieResult.

@rtyley rtyley changed the title Update Cookie generation and parsing code Update Cookie generation & parsing code Aug 7, 2024
@rtyley rtyley changed the base branch from main to update-settings-loading-and-parsing-code August 7, 2024 16:43
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from c11c67f to 8592c75 Compare August 7, 2024 16:47
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch from 65ca26c to c081b7e Compare August 7, 2024 16:56
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from 8592c75 to a0f4f81 Compare August 7, 2024 16:58
@rtyley rtyley changed the title Update Cookie generation & parsing code Update Cookie generation-&-parsing code Aug 7, 2024
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch 3 times, most recently from 350d62d to 4b540ec Compare August 7, 2024 22:16
@rtyley rtyley changed the title Update Cookie generation-&-parsing code Refactor Cookie generation-&-parsing Aug 8, 2024
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from 4b540ec to 6c4c583 Compare August 8, 2024 08:39
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch 3 times, most recently from 0630bdd to e1fcd7a Compare August 8, 2024 09:33
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from 6c4c583 to 9f20fc6 Compare August 8, 2024 09:41
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch from e1fcd7a to d565dd6 Compare August 8, 2024 10:02
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from 9f20fc6 to ab80840 Compare August 8, 2024 10:03
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch from d565dd6 to 3cd167f Compare August 8, 2024 10:58
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from ab80840 to ec6ca77 Compare August 8, 2024 10:58
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch from 3cd167f to af01b6b Compare August 8, 2024 11:00
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from ec6ca77 to efcee19 Compare August 8, 2024 11:09
@rtyley rtyley force-pushed the update-settings-loading-and-parsing-code branch from af01b6b to c483501 Compare August 8, 2024 13:51
@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from efcee19 to c06d3fd Compare August 9, 2024 14:03
* 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
Copy link
Contributor

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?

Copy link
Member Author

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:

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...

Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

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...

Copy link
Member Author

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
Copy link
Contributor

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?

Copy link
Member Author

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 {
Copy link
Contributor

@bryophyta bryophyta Aug 9, 2024

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 👍

@rtyley rtyley force-pushed the update-cookie-generation-and-parsing-code branch from c06d3fd to a8e3840 Compare August 9, 2024 14:47
@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)
Copy link
Member Author

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.

scalatest/scalatest#1775 (comment)

Comment on lines -44 to -51
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)) {
Copy link
Member Author

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
Copy link
Member Author

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)
@rtyley rtyley marked this pull request as ready for review August 9, 2024 15:20
@rtyley rtyley requested a review from a team as a code owner August 9, 2024 15:20
Copy link
Contributor

@bryophyta bryophyta left a 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 👍

Base automatically changed from update-settings-loading-and-parsing-code to main September 4, 2024 09:22
@rtyley rtyley merged commit 65e1f65 into main Sep 4, 2024
1 check passed
@rtyley rtyley deleted the update-cookie-generation-and-parsing-code branch September 4, 2024 09:23
rtyley added a commit to guardian/login.gutools that referenced this pull request Sep 18, 2024
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.
rtyley added a commit to guardian/login.gutools that referenced this pull request Sep 19, 2024
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.
rtyley added a commit to guardian/giant that referenced this pull request Nov 8, 2024
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.
rtyley added a commit to guardian/giant that referenced this pull request Nov 8, 2024
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.
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.

2 participants