aidantwoods / swift-paseto

Platform-Agnostic Security Tokens implementation in Swift
MIT License
22 stars 5 forks source link

Support nested data structures #6

Closed davidperrenoud closed 2 years ago

davidperrenoud commented 5 years ago

I tried decoding a public token corresponding to the following nested data structure:

{
  "iss": "paragonie.com",
  "sub": "test",
  "aud": "pie-hosted.com",
  "exp": "2039-01-01T00:00:00+00:00",
  "iat": "2038-03-17T00:00:00+00:00",
  "jti": "87IFSGFgPNtQNNuw0AtuLttP",
  "my": {
    "nested": "structure"
  }
}

But this results in Message.verify throwing an exception, as the decoder tries to map all tokens to [String: String]. I do not see anything in the PASETO spec discouraging these kinds of structures. Do you plan on supporting them?

aidantwoods commented 5 years ago

I think this originated from parts of the reference implementation source (prior to the formal spec being written to clarify), where array<string, string> was used e.g. https://github.com/paragonie/paseto/blob/52c094dd4f6689fa5ba5352dc3d87c0f31955f9a/src/JsonToken.php#L189-L199

You're correct about PASETO spec allowing nested structures though, the spec even gives one as an example: {"foo":"bar","baz":12345,"678":["a","b","c"]} (with the restriction that the top layer must be a dictionary).

Likely the simplest fix is to do something like change: [String: String] to [String: Any], though probably nicer to work with (and certainly more specific than Any) would be to implement something like the JSON enum given as an example in SE-0195, probably also utilising @dynamicMemberLookup. To my knowledge there's no type in the standard library that was ever added to already implement this use-case for working with JSON, unless you know of anything?

aidantwoods commented 2 years ago

I'm taking a look at fixing this.

My initial recent thoughts were that I could have Token accept a generic T: Codable that could be used. Unfortunately I think this isn't really workable, particularly with regard to adding "claims" to an already built token (i.e. if claims is T: Codable, how to construct a new type S at runtime such that S has all the properties of T plus a new claim key). If both types were defined it might be possible to write a mapper between the two types (which adds data to S upon creation), but this seems like it might be difficult to use in practice. For adding of validator support, this would also make it hard to vet the standardised claims if a custom T is supplied (especially given that Codables can decode with different key names than the original JSON had). I think the only sane way to vet claims would be to hang onto the original JSON string and decode many times depending on which T was passed (again, this seems kind of non-ideal).

I'd like to avoid [String: Any], so my first go will be to try implementing this using an extended version of the JSON type from SE-0195.

aidantwoods commented 2 years ago

My initial first go at this involves introducing a new type, simplified form:

public enum JSON {
    @dynamicMemberLookup
    public enum Container: Equatable {
        case Array([Value])
        case Dictionary([String: Value])

        public subscript(dynamicMember member: String) -> Value? {
            if case .Dictionary(let dict) = self {
                  return dict[member]
            }

            return nil
        }
    }

    /**
     * JSON.Value is based on the types that may be returned from JSONSerialization.
     *
     * Apple's documentation notably omits the boolean case, and does not distinguish numbers by type.
     */
    public enum Value: Equatable {
        case Container(Container)
        case String(String)
        case Number(NSNumber)
        case Null
    }
}

JSON.Value and JSON.Container then implement the various ExpressibleBy*Literal protocols so that values can be constructed without much hassle.

The claims property in Token would then be changed from [String: String] to [String: JSON.Value], which ensures that the top level is still a dictionary (as required by Paseto spec), but then allows JSON values to be nested.

Still a bit of a WIP, I've extracted the JSON implementation out into its own repo: https://github.com/aidantwoods/TypedJSON

aidantwoods commented 2 years ago

This isn't in a release version yet, but closing this now as it'll be in 1.0