File: encoders.md

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (142 lines) | stat: -rw-r--r-- 4,292 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
---
layout: guide
doc_stub: false
search: true
section: GraphQL Pro
title: Encrypted, Versioned Cursors and IDs
desc: Increased opacity and configurability for Relay identifiers
index: 6
pro: true
---

`GraphQL::Pro` includes a mechanism for serving encrypted, versioned cursors and IDs.  This provides some benefits:

- Users can't reverse-engineer node IDs or connection cursors, removing a possible attack vector.
- You can gradually transition between cursor strategies, adding encrypting while supporting any "stale" encoders which clients already have.

`GraphQL::Pro`'s encrypted encoders provide a few security features:

- Key-based encryption by `aes-128-gcm` by default
- Authentication
- Nonces for cursors (but not IDs, that would be silly)

## Defining an Encoder

Encoders can be created by subclassing `GraphQL::Pro::Encoder`: 

```ruby
class MyEncoder < GraphQL::Pro::Encoder
  key("f411f30...")
  # optional:
  tag("81ce51c307")
end
```

- `key` is the encryption key for this encoder. You can generate one with: `require "securerandom"; SecureRandom.bytes(16)`
- `tag`, if provided, is used as authentication data or for disambiguating versioned encoders

## Encrypting Cursors

Encrypt cursors by attaching an encrypted encoder to `Schema#cursor_encoder`:

```ruby
class MySchema GraphQL::Schema
  cursor_encoder(MyCursorEncoder)
end
```

Now, built-in connection implementations will use that encoder for cursors.

If you implement your own connections, you can access the encoder's encryption methods via {{ "GraphQL::Pagination::Connection#encode" | api_doc }} and {{ "GraphQL::Pagination::Connection#decode" | api_doc }}.


## Encrypting IDs

Encrypt IDs by using encoders in `Schema.id_from_object` and `Schema.object_from_id`:

```ruby
class MySchema < GraphQL::Schema
  def self.id_from_object(object, type, ctx)
    id_data = "#{object.class.name}/#{object.id}"
    MyIDEncoder.encode(id_data)
  end

  def self.object_from_id(id, ctx)
    id_data = MyIDEncoder.decode(id)
    class_name, id = id_data.split("/")
    class_name.constantize.find(id)
  end
end
```

Note that IDs are _not_ encrypted with nonces. This means that if someone can _guess_ how IDs are constructed, they can determine the encryption key (a kind of [known-plaintext attack](https://en.wikipedia.org/wiki/Known-plaintext_attack)). To reduce this risk, make your plaintext IDs unpredictable, for example, by appending a salt or obfuscating their content.

## Versioning

You can combine several encoders into a single chain of versioned encoders. Pass them to `.versioned`, newest-to-oldest:

```ruby
# Define some encoders ...
class NewSecureEncoder < GraphQL::Pro::Encoder
  # ...
end

class OldSecureEncoder < GraphQL::Pro::Encoder
  # ...
end

class LegacyInsecureEncoder < GraphQL::Pro::Encoder
  # ...
end

# Then order them by priority:
VersionedEncoder = GraphQL::Pro::Encoder.versioned(
  # Newest:
  NewSecureEncoder,
  OldSecureEncoder,
  # Oldest:
  LegacyInsecureEncoder
)
```

When receiving an ID or cursor, a versioned encoders tries each encoder in sequence. When creating a new ID or cursor, the encoder always uses the first encoder. This way, clients will receiving _new_ encoders, but the server will still accept _old_ encoders (until the old one is removed from the list).

`VersionedEncoder#decode_versioned` returns two values: the decoded data _and_ the encoder which successfully decoded it. You can use this to determine how to process decoded data. For example, you can switch on the encoder:

```ruby
data, encoder = VersionedEncoder.decode_versioned(id)
case encoder
when UUIDEncoder
  find_by_uuid(data)
when SQLPrimaryKeyEncoder
  find_by_pk(data)
when nil
  # `id` could not be decoded
  nil
end
```

## Encoding

By default, encrypted bytes is stringified as base-64. You can specific a custom encoder with the `Encoder#encoder` definition. For example, you could define an encode which uses URL-safe base-64 functions:

```ruby
module URLSafeEncoder
  def self.encode(str)
    Base64.urlsafe_encode64(str)
  end
  def self.decode(str)
    Base64.urlsafe_decode64(str)
  end
end
```

Then attach it to your encoder:

```ruby
class MyURLSafeEncoder < GraphQL::Pro::Encoder
  encoder URLSafeEncoder
end
```

Now, these node IDs and cursors will be URL-safe!