···11-## Service Authentication (serviceAuth)
11+Ever wondered how you can prove to another service that you're who you say you are? That you are the logged-in
22+atmosphere account? Remember way back on day 1 when we had you grab the `multikey` from your DID document under
33+`verificationMethod`? That's actually the public key to the private key on the PDS that is used to sign the commits to
44+your repo. It can also be used to create a JWT to prove it's you by the signature using the
55+[com.atproto.server.getServiceAuth](https://docs.bsky.app/docs/api/com-atproto-server-get-service-auth) endpoint.
2633-In the AT Protocol, services authenticate requests between each other using **serviceAuth** — a signed JWT token. When your PDS proxies a request to another service on your behalf, it creates a JWT signed with your account's signing key.
77+When calling this endpoint there are 2 required parameters:
4855-For this challenge, you'll create a serviceAuth JWT yourself. Here's what you need:
99+- `aud`: This is the `did:web` of the service you expect to use this token. An example is when migrating to create a new
1010+ account on the PDS with your did it would be the PDS's did like `did:web:blacksky.app`.
1111+- `lxm`: The XRPC endpoint you are calling, like `com.atproto.server.createAccount` in the case of a migration.
61277-### JWT Structure
1313+The JWT created looks a bit like this decoded.
81499-Your JWT must have these **header** fields:
1010-- `alg`: `ES256` (if your signing key is P-256) or `ES256K` (if secp256k1)
1111-- `typ`: `JWT`
1515+header:
12161313-And these **payload** claims:
1414-- `iss`: Your DID (e.g. `did:plc:abc123...`)
1515-- `aud`: `{{service_did}}`
1616-- `lxm`: `{{lxm_part_one}}`
1717-- `exp`: A UNIX timestamp in the future (current time + 60 seconds works)
1818-- `iat`: Current UNIX timestamp
1717+```json
1818+{
1919+ "typ": "JWT",
2020+ "alg": "ES256K"
2121+}
2222+```
19232020-Sign it with your account's **repo signing key** — the same key listed in your DID document's verification method.
2424+Payload:
21252222-The resulting JWT is three base64url-encoded segments joined by dots: `header.payload.signature`
2626+```json
2727+{
2828+ "iat": 1774113399,
2929+ "iss": "did:plc:rnpkyqnmsw4ipey6eotbdnnf",
3030+ "aud": "did:web:blacksky.app",
3131+ "exp": 1774113459,
3232+ "lxm": "com.atproto.server.createAccount",
3333+ "jti": "793cd20048417bcd8d229f343a95363f"
3434+}
3535+```
3636+3737+On the backend usually you decode the JWT, accessed the `iss` claim, resolve the did document, and from there can then
3838+verify the signature.
23392424-### Submit Your JWT
4040+In the AT Protocol, services authenticate requests between each other using **serviceAuth** — a signed JWT token. When
4141+your PDS proxies a request to another service on your behalf, it creates a JWT signed with your account's signing key.
25422626-Paste the raw JWT string below and we'll verify the signature against your DID document.
4343+For this challenge request a serviceAuth token for your account for the lxm `{{lxm_part_one}}` endpoint with the `aud`
4444+of `{{service_did}}`.
+23-13
shared/src/advent/challenges/day_six.rs
···22use crate::OAuthAgentType;
33use crate::advent::day::Day;
44use crate::advent::{AdventChallenge, AdventError, AdventPart, ChallengeCheckResponse};
55-use crate::atrium::service_auth::{decode_and_verify_service_auth, decode_jwt_claims, extract_signing_key_bytes};
55+use crate::atrium::service_auth::{
66+ decode_and_verify_service_auth, decode_jwt_claims, extract_signing_key_bytes,
77+};
68use async_trait::async_trait;
79use atrium_api::types::string::Did;
810use atrium_common::resolver::Resolver;
911use serde_json::json;
1012use sqlx::PgPool;
11131212-pub const LXM_PART_ONE: &str = "codes.advent.challenge.verifyDay6";
1313-pub const LXM_PART_TWO: &str = "codes.advent.challenge.getDay6Code";
1414+pub const LXM_PART_ONE: &str = "codes.advent.challenge.exampleEndpoint";
1515+pub const LXM_PART_TWO: &str = "codes.advent.challenge.getChallengeCode";
14161517pub struct DaySix {
1618 pub pool: PgPool,
···4850 _code: &str,
4951 ) -> Result<Option<serde_json::Value>, AdventError> {
5052 // Strip "did:web:" prefix for the domain used in URLs
5151- let domain = self.service_did.strip_prefix("did:web:").unwrap_or(&self.service_did);
5353+ let domain = self
5454+ .service_did
5555+ .strip_prefix("did:web:")
5656+ .unwrap_or(&self.service_did);
5257 Ok(Some(json!({
5358 "service_did": self.service_did,
5459 "service_did_domain": domain,
···117122 let iss_did: Did = claims.iss.parse().map_err(|_| {
118123 AdventError::ShouldNotHappen(format!("Invalid DID in JWT iss: {}", claims.iss))
119124 })?;
120120- let did_doc = self.handle_resolver.resolve(&iss_did).await.map_err(|err| {
121121- log::error!("Failed to resolve DID document for {}: {}", claims.iss, err);
122122- AdventError::ShouldNotHappen(format!("Failed to resolve DID document: {err}"))
123123- })?;
125125+ let did_doc = self
126126+ .handle_resolver
127127+ .resolve(&iss_did)
128128+ .await
129129+ .map_err(|err| {
130130+ log::error!("Failed to resolve DID document for {}: {}", claims.iss, err);
131131+ AdventError::ShouldNotHappen(format!("Failed to resolve DID document: {err}"))
132132+ })?;
124133125134 let (key_alg, key_bytes) = match extract_signing_key_bytes(&did_doc) {
126135 Ok(result) => result,
···163172 ))
164173 }
165174 Some(challenge) => {
166166- let expected = challenge
167167- .verification_code_two
168168- .ok_or(AdventError::ShouldNotHappen(
169169- "No verification code for part two".to_string(),
170170- ))?;
175175+ let expected =
176176+ challenge
177177+ .verification_code_two
178178+ .ok_or(AdventError::ShouldNotHappen(
179179+ "No verification code for part two".to_string(),
180180+ ))?;
171181172182 if submitted_code == expected {
173183 Ok(ChallengeCheckResponse::Correct)