Add the 51Did creator context web example - #113
Merged
Conversation
Adds the web example for the 51Did creator context feature under pipeline.developer-examples/pipeline.developer-examples.fodid, beside the existing examples there, and lets every cloud example in this repository be pointed at another host through the endpoint environment variable the READMEs describe.
The did package gains DidClient, one type for everything a server does
with a 51Did against the 51Degrees cloud, so that server code never
hand-writes HTTP or key handling. It fetches the published signing keys
once and holds them, picks the key in force when a 51Did was created
(with the cloud's tolerance either side of a key boundary), verifies a
signature offline against that key or through the cloud's verify
endpoint, and redeems a sealed creator context result with the licence
key into a typed RedeemResult. The resource key travels in the route
and the licence key only in the POST form body, never in a URL. An
HttpTransport interface over HttpURLConnection lets tests stand in for
the network. JSON is read with org.json at the version the pipeline's
cloud request engine already uses.
FodId now parses both base64 alphabets, so the URL-safe form a page
puts in a link is accepted, and adds asBase64Url() and getDateMinutes().
The License Id documentation says that on an identifier carrying a
creator context the field holds an encrypted value only 51Degrees can
read. The reader's minimum length rule is unchanged.
The creator context example's /redeem route uses the client instead of
building the cloud URL itself. It parses the incoming 51Did, checks the
signature offline, redeems with the licence key, and answers the page in
the cloud's own shape with one field added, serverSignature. The page
is unchanged. The example readme shows the client calls and the package
readme gains a "Verifying on your server" section.
Files changed
pipeline.did/pom.xml, README.md
pipeline.did/src/main/java/fiftyone/pipeline/did/
DidClient.java, DidHttpException.java,
DidNotSupportedException.java, FodId.java, HttpTransport.java,
RedeemResult.java, SigningKey.java, package-info.java
pipeline.did/src/test/java/fiftyone/pipeline/did/
DidClientTests.java, DidClientLiveTests.java,
FodIdTestFactory.java, FodIdTests.java
pipeline.developer-examples/pipeline.developer-examples.fodid/
pom.xml, README.md, CreatorContextDemoServer.java,
ExampleTests.java
Tests
pipeline.did: 92 run, 0 failed, 2 skipped (the live cloud tests,
which run only with a resource key), up from 32.
pipeline.developerexamples.fodid: 9 run, 0 failed, up from 1.
The redeem controller's POST route carries no resource segment, so a
POST to id/redeem/{resource} answers 404 and the resource key is read
from the form body instead. DidClient.redeem now posts to the bare
id/redeem path with resource, 51did, result, challenge and license all
in the form. The GET calls (id/key/{resource}, id/verify/{resource})
are unchanged. The tests that assert the redeem URL and form, and the
class documentation on where each credential travels, are updated to
match.
Tests
pipeline.did: 92 run, 0 failed, 2 skipped (live tests without a
resource key).
pipeline.developerexamples.fodid: 9 run, 0 failed.
The verify endpoint reads the identifier as 51did and keeps owid as an alias, but a cloud that has not taken the creator context release reads owid only and answers 400 "Can not verify owid" to a request carrying 51did alone. DidClient.verify now sends the identifier under both names in the query string, so it works against either. Redeem is unchanged, because it exists only on the updated cloud. The test asserting the verify URL is updated to match. Tests pipeline.did: 92 run, 0 failed, 2 skipped (live tests without a resource key).
Describe the verify parameter aliases and key start fields as stable compatibility behavior.
jwrosewell
force-pushed
the
feature/51did-creator-context-example
branch
from
August 28, 2026 15:14
058f28d to
9f5b6f1
Compare
Return the required key fetch error instead of checking a signature against a schedule that may not contain the correct key.
The package carried a public maximum envelope length, a private payload maximum and a base64 character maximum worked out from it, and every factory refused anything longer. All of that is removed, along with the README paragraph that stated the maximum, the error messages that quoted the figures and the tests that asserted them. The reader keeps the lower bound it always had, which is that a payload must be at least the base length for its identifier type, so anything longer is accepted and left to the cloud to judge. The maximum was wrong on two counts. The creator domain is a deployment parameter, so a self-hosted container may sign with a longer domain and its identifiers must still parse. The service also accepts a context section of a version it does not implement at any length, so an older verifier keeps working when a newer version ships, and the package has to behave the same way. What replaced it is one generous bound in the client, not in the reader. An encoded identifier longer than 4096 characters is refused before the client decodes it, fetches a key or calls the cloud. That figure is arbitrary and far above anything the cloud issues, because its only job is to turn away obviously malformed input. Two defects in the same code are fixed. Leading and trailing whitespace is now stripped from an encoded identifier before it is measured and before the alphabet is converted, so a valid identifier with a trailing newline, a leading space or a trailing space parses to the same value as the clean form. The base64 padding is worked out from the stripped length rather than the raw one, which was already wrong before the maximum was added. The dead length helpers in FodId and DidClient go too, because the constructor already enforces the remaining rules and an OWID signature is always read as a fixed 64 bytes, so nothing reachable through the public API could make them fail. The README no longer contradicts itself, as it said in one place that the context section may be any length and in another gave a fixed envelope maximum. Tests before: 101 run, 0 failures, 2 skipped. Tests after: 99 run, 0 failures, 2 skipped.
The boundary tests used offsets of five and twenty minutes, which narrowed the allowance to a small range for anyone reading them. The offsets now sit a minute inside and an hour outside it, so each test still proves which key is tried on its side of a boundary without recording how wide the allowance is. The two offsets are named JUST_INSIDE and WELL_OUTSIDE so the point of each one is clear everywhere they are used.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The 51Did creator context feature in two parts, being the web example that shows the flow and the cloud client in
pipeline.didthat the example's server now uses.The creator context web example
Adds
pipeline.developer-examples/pipeline.developer-examples.fodid'sCreatorContextDemoServer, a small server on the JDK's own web server that servespage.html. The page creates a 51Did in the browser through the cloudjsonendpoint, verifies it from the browser withverify-full(which answers with a sealedresultthe browser cannot read or forge), and hands the sealed result to the server's/redeemroute, which redeems it with the licence key the browser never sees. Once the identifier validates, the page offers a link carrying the same 51Did, and opening that link in another browser shows the signature still verifying while the creator context does not, which is the copied identifier caught at presentation. The same commit lets every example that calls the cloud be pointed at another host throughFOD_CLOUD_API_URL, the variable the cloud request engine already honours, with the resource key from_51DEGREES_RESOURCE_KEY. The cloud engine example reads both (falling back to the star sign service it was written for) and reports plainly when the host offers no star sign product, and the root README describes the variables.The cloud client, used by the example
pipeline.didgainsDidClient, one type for everything a server does with a 51Did against the 51Degrees cloud, so server code never hand-writes HTTP or key handling. New public surface:FodId.fromBase64now accepts the URL-safe base64 alphabet (-,_, padding optional) as well as the standard one the cloud issues, normalising before the OWID library sees it, because that library's decoder ignores characters outside the standard alphabet rather than refusing them. Whitespace around the value is stripped at the same point, so an identifier read from a header, a file or a form field with a newline or a space around it reads as the same identifier as the clean form, and the base64 padding is worked out from the stripped length.FodId.asBase64Url()gives the URL-safe form without padding, andFodId.getDateMinutes()gives the envelope's own date field, the unsigned count of minutes since 2020-01-01T00:00:00Z.getLicenseId()is documented as the raw field, which on an identifier carrying a creator context holds an encrypted value only 51Degrees can read. The minimum length rule is unchanged.DidClient(resourceKey),DidClient(resourceKey, licenceKey),DidClient(resourceKey, licenceKey, endpoint)andDidClient.builder(resourceKey)(which also takes anHttpTransportand aClock). The endpoint defaults tohttps://cloud.51degrees.com/api/v4/, orFOD_CLOUD_API_URLwhere set, and a trailing slash is added where missing.publicKeys()fetchesGET id/key/{resource}once and holds the list, readingstartsAt(or the compatibility fieldcreatedwhenstartsAtis absent) andpublicKey.publicKeyFor(fodId)returns the key in force at the identifier's date, refetching once when the held list has no key for the date, when the date is later than the newest start held, or when the list is more than a day old. A required refresh failure propagates because the held schedule may not contain the correct key.verifySignature(fodId)andverifySignatureDetailed(fodId)check the signature offline the way the cloud's verify endpoint does: version 3 only, the payload at least the base length for its type (a longer payload carries a creator context section and is accepted), and the key in force at the identifier's date plus the neighbouring key within the cloud's tolerance either side of a boundary, never every earlier key.verify(fodId)callsGET id/verify/{resource}?51did=and returns the cloud'svalid, raisingIllegalArgumentExceptionwith the cloud's message for a value that is not a 51Did. The identifier is sent under both parameter names,51didandowid, so the request works with hosts that read either parameter. Hosts that recognise both prefer51didand keepowidas a compatibility alias.redeem(fodId, result, challenge)sendsPOST id/redeem(the bare path, because the POST route carries no resource segment) withresource,51did,result,challengeandlicensein the form body (neither key is ever in a URL) and returns a typedRedeemResultfor a 200 or 503 answer:Context(VERIFIED,MISMATCH,NO_CONTEXT,NOT_CHECKABLE,EXPIRED,REPLAYED,UNREADABLE,UNCONFIRMED, with any unknown word mapped toUNREADABLEand kept ingetContextValue()),Signature(VERIFIED,INVALID,UNKNOWN),getFactors()when the cloud sent them,getVerifiedAt(),getSecondsSinceVerified(),getStatusCode()andgetRaw(). A 400 raisesIllegalArgumentException, a 404 raisesDidNotSupportedException(the host does not offer the creator context), any other status raisesDidHttpExceptionwith the status and body, and an unreachable host raisesIOException.HttpTransport, an interface overHttpURLConnectionwithRequestandResponsetypes, so tests inject the network.SigningKeyis one entry of the key list.Every request carries a
User-Agentofpipeline.did/{version}from the jar manifest. The package addsorg.json:jsonat the versionpipeline.cloudrequestenginealready uses, so the pipeline carries no new dependency. Everything compiles at the Java 8 level and Animal Sniffer checks the API.The example's
/redeemroute (CreatorContextDemoServer.redeem) now builds oneDidClientat start-up, parses the incoming 51Did withFodId.fromBase64, callsverifySignature, thenredeem, and answers the page in the cloud's own shape (signature,context,factorswhen present,verifiedAt,secondsSinceVerified) with one field added,serverSignature.page.htmlis unchanged. A host without the creator context still answers 404 with a text body, which the page reports as not supported by this host, and an unreachable cloud answers 502 with{ "error": ... }. The example readme's copy-these-lines section shows the client calls, and the package readme gains a "Verifying on your server" section covering parse, offline verify, cloud verify and redeem, with a note thatverify-contextandverify-fullare browser calls because the context describes the browser's own connection.Tests
Unit tests use the injected transport and a real ECDSA key pair, with no network. They cover both base64 alphabets and the round trip,
getDateMinutes, the key list (startsAt, thecreatedfallback, cache hit, each refetch rule and a failed required refresh with an injected clock), key selection at and around a boundary, offline verification (right key, wrong key, version 2, short payload, payload with a context section, a long context section signed with a longer creator domain, random identifier), cloud verification (200 valid, 400 invalid, 400 errors), and every redeem outcome including 503, 400, 404, an unknown context word, the form fields, and the licence key's absence from the URL. The example's route is tested for the cloud's shape withserverSignature, factors, 503, 404, 502 and 400. Two live tests run only when_51DEGREES_RESOURCE_KEYis set and are skipped otherwise. Further tests cover an identifier arriving with a leading space, a trailing space or a trailing newline in either alphabet, a long creator context section and a longer creator domain reading back through every factory, and the client turning away an over-long encoded value through both its string and its object entry points without making a request.pipeline.didpipeline.developerexamples.fodidBoth modules build, with
mvn -pl pipeline.did -am testandmvn -pl pipeline.developer-examples/pipeline.developer-examples.fodid -am package.Identifier size is left to the cloud
The package states no maximum length for a 51Did, and the reader keeps only the lower bound it has always had, which is that a payload must be at least the base length for its identifier type. Anything longer is accepted and left to the cloud to judge. Two reasons make a fixed maximum in the package wrong. The creator domain is a deployment parameter, so a self-hosted container may sign with a longer domain than the cloud does and its identifiers must still parse. The service also accepts a creator context section of a version it does not implement at any length, which is what lets an older verifier keep working when a newer version ships, and the package has to behave the same way.
DidClientkeeps one generous bound so it does not do work on input that cannot be an identifier at all. An encoded identifier longer than 4096 characters is refused before the client decodes it, fetches a key or calls the cloud. That figure is arbitrary and far above anything the cloud issues, and its only job is to turn away obviously malformed input. Public method signatures are unchanged, and cloud response bodies are not capped by any of this.