Class: Ksef::Auth::Token

Inherits:
Object
  • Object
show all
Defined in:
lib/ksef/auth/token.rb

Overview

A KSeF token and the context it authenticates into — the credential object of DESIGN.md §8:

Ksef::Auth::Token.new(context_nip: "9999999999", token: ENV["KSEF_TOKEN"])

A KSeF token is the second of the API's two authentication methods (docs/REFERENCE.md §4). It is not a bearer token and is never sent as one: it is RSA-OAEP-encrypted together with the challenge's timestamp and posted to POST /auth/ksef-token, which starts the same asynchronous operation the XAdES flow does and ends at the same POST /auth/token/redeem.

It cannot be the first credential

A KSeF token can only be issued after a one-time XAdES authentication (tokeny-ksef.md; POST /tokens requires a bearer, /auth/xades-signature requires nothing). So this class is the everyday path and Signer is the bootstrap — which is why the certificate flow shipped first (§6a.2).

Treat instances as secrets. #to_s and #inspect are redacted, and the token is reachable only by the deliberate #authentication_request.

Constant Summary collapse

REDACTED =
"[REDACTED]"
CONTEXT_TYPES =

Only two of the contract's four AuthenticationContextIdentifierType values are reachable here, and that is not a simplification: tokeny-ksef.md records that a token can only be issued in a Nip or InternalId context, so a token for the other two cannot exist to be presented (§4.1).

{ nip: "Nip", internal_id: "InternalId" }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token:, context_nip: nil, internal_id: nil) ⇒ Token

Returns a new instance of Token.

Parameters:

  • token (String) —

    the KSeF token, verbatim from POST /tokens

  • context_nip (String, nil) (defaults to: nil) —

    the NIP to authenticate into

  • internal_id (String, nil) (defaults to: nil) —

    an InternalId context instead — <nip>-<5 digits>

Raises:



40
41
42
43
44
# File 'lib/ksef/auth/token.rb', line 40

def initialize(token:, context_nip: nil, internal_id: nil)
  @token = validate_token(token)
  @context_type, @context_value = resolve_context(context_nip, internal_id)
  freeze
end

Instance Attribute Details

#context_type ⇒ Object (readonly)

Returns the value of attribute context_type.



34
35
36
# File 'lib/ksef/auth/token.rb', line 34

def context_type
  @context_type
end

#context_value ⇒ Object (readonly)

Returns the value of attribute context_value.



34
35
36
# File 'lib/ksef/auth/token.rb', line 34

def context_value
  @context_value
end

Instance Method Details

#authentication_request(challenge:, certificate:, allowed_ips: nil) ⇒ Hash

The InitTokenAuthenticationRequest body for POST /auth/ksef-token.

Built here rather than inside Client for the same reason Client#submit_xades takes an already-signed document: the HTTP layer maps requests and responses, and the credential is what knows how to present itself.

Parameters:

Returns:

  • (Hash)


62
63
64
65
66
67
68
69
70
71
# File 'lib/ksef/auth/token.rb', line 62

def authentication_request(challenge:, certificate:, allowed_ips: nil)
  request = {
    challenge: Challenge.validate_format!(challenge.to_s),
    contextIdentifier: context_identifier,
    encryptedToken: encrypted_token(challenge: challenge, certificate: certificate),
    publicKeyId: certificate.public_key_id
  }
  policy = AuthorizationPolicy.coerce(allowed_ips)
  policy ? request.merge(authorizationPolicy: { allowedIps: policy.to_h }) : request
end

#context_identifier ⇒ Object

The contract's AuthenticationContextIdentifier.



47
# File 'lib/ksef/auth/token.rb', line 47

def context_identifier = { type: CONTEXT_TYPES.fetch(context_type), value: context_value }

#encrypted_token(challenge:, certificate:) ⇒ String

{ksefToken}|{timestampMs}, UTF-8, RSA-OAEP-encrypted and base64-encoded (§4.5).

The timestamp is not decoration and not ours to generate: the docs are explicit that it acts as a nonce, so that a captured ciphertext cannot be replayed into a later session. It has to be the timestampMs the challenge response carried, which is why this takes a Challenge and not a String — a locally generated millisecond count will not match, and the authentication fails with nothing to point at.

Returns:

  • (String) —

    base64



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ksef/auth/token.rb', line 83

def encrypted_token(challenge:, certificate:)
  timestamp = challenge.respond_to?(:timestamp_ms) ? challenge.timestamp_ms : nil
  if timestamp.nil?
    raise AuthenticationError,
          "The KSeF-token flow needs the challenge's own timestampMs, so pass the " \
          "Ksef::Auth::Challenge returned by POST /auth/challenge rather than its string. " \
          "The timestamp is a replay nonce (docs/REFERENCE.md §4.5)."
  end

  Crypto.encode(certificate.encrypt("#{@token}|#{timestamp}"))
end

#inspect ⇒ Object



97
98
99
# File 'lib/ksef/auth/token.rb', line 97

def inspect
  "#<Ksef::Auth::Token context=#{CONTEXT_TYPES.fetch(context_type)}:#{context_value} token=#{REDACTED}>"
end

#to_s ⇒ Object



95
# File 'lib/ksef/auth/token.rb', line 95

def to_s = REDACTED