DHL Paket API
This repository contains findings on how the REST API of the DHL Paket App works as well as a handful of ruby scripts that demonstrate the usage of the API. The DHL Paket API enables a registered user to retrieve information about shipments from/to the user as well as the current mTan which is required to pick up shipments from a Packstation.
The focus lies on the parts of the API that are used to track shipments and retrieve the current mTan; the part of the API that handles purchasing of stamps and other services is out of scope.
API
Headers
Almost all request to the API require a specific set of headers to be present:
- Client_id: OAuth client id, same for each request.
- Interface-Key: hard-coded value as used in the Android app
- Emmi-Api-Version: 7
- Authorization: Bearer {ACCESS_TOKEN}
Valid values for Client_id
and Interface-Key
headers can be found in the common.rb file.
The headers must be included for all requests, unless stated otherwise.
Authentication
The authentication is performed via OAuth2 with RFC7636: The app opens the authentication URL https://mobil.dhl.de/oauth-web/oauth/grant
in a embedded browser and waits for the user to authenticate. Upon successful authentication, the user is "redirected" to the non-existent URL which is intercepted by the browser. The URL is then transformed such that the parameters specified in the fragment of the URL can be extracted. The parameters contain the short-lived authorization code, which can be exchanged for the long-lived Access and Refresh Tokens.
The required steps to authenticate against the API are outlined in the following paragraphs
Step 1: Open the authentication URL in a browser
The authentication URL is constructed in the following way:
https://mobil.dhl.de/oauth-web/oauth/grant?response_type=code&client_id={CLIENT_ID}&scope={SCOPE}&state={STATE}&code_challenge={CODE_CHALLENGE}&code_challenge_method={CODE_CHALLENGE_METHOD}
CLIENT_ID
, SCOPE
and CODE_CHALLENGE_METHOD
are fixed values that are hardcoded in the app. You can find appropriate values from the Android App in the file common.rb.
STATE
is a (in this case randomly generated base64) string that is passed to and returned back from the autorization server. In practice it's used to prevent CSRF attacks.
CODE_CHALLENGE
is the hash (generated with the method specified in CODE_CHALLENGE_METHOD
, in this case SHA256) of a randomly generated byte sequence. It's encoded with a special base64-dialect, as specified in RFC7636 (urlsafe base64).
Step 2: Extract authorization code
After successful authentication, the user will be redirected to a non-existent URL similar to the following:
https://app.dhl.de/android-dhl-parcel#state=....=&code=eyJra...long...code
As you can see, the fragment of the URL contains the relevant data. The value of the state
parameter should equal the value of the state
parameter that was specified in the previous step.
The code
parameter contains the encrypted, base64-encoded authorization code which can be exchanged for the Access and Refresh Tokens in the next step.
Step 3: Exchange authorization code for Access and Refresh Tokens
The authorization code obtained in the previous step cannot be used for authentication directly. It must be exchanged for Access and Refresh Tokens.
To do this, a POST request must be sent to the URL https://app.dhl.de/oauth/grant/exchange
.
The body of the request consists of a JSON object that only contains a single attribute: code_verifier which contains the byte sequence that was used to generate the CODE_CHALLENGE
hash in Step 1. It's encoded with the same base64-dialect as the hash.
The request must include header described in the "Headers" section, with one exception:
The Authorization header must have the following format:
Authorization: Grant {CODE}
Additionally, the code verifier already included in the body should be conveyed in the header, too:
Code_verifier: {verifier}
(same as the code_verifier value in the body)
If successful, the response will contain a JSON-object with three fields: accessToken, refreshToken and accessValidity. accessValidity contains the duration during which the accessToken is valid. It's specified in Milliseconds. Default: 3600000, 1h.
Congratulations, you successfully retrieved the tokens required to authenticate against the API.
Step 4: Refresh Access Token
As indicated by the accessValidity field from Step 3, the obtained access token has a limited validity. After the specified duration, the Access Token cannot be used and API calls will result in a 401 response.
In this case, the Refresh Token can be used to retrieve a new Access and Refresh Token:
A POST request must be sent to the URL https://app.dhl.de/oauth/token/request
. The body of the request may be empty, the relevant information is conveyed using the headers:
Again, the headers described in the section "Headers" must be included, with one excpetion: The Authorization header must have the following format:
Authorization: Refresh {REFRESH_TOKEN}
The response will have the same format as the response in Step 3. It will contain a new Access Token and a new Refresh Token.
Retrieving Shipments
To retrieve a list of current shipments, send a POST request to https://app.dhl.de/shipments
.
The body should consist of the following JSON structure to retrieve a list of what the app considers to be current shipments.
{
"shipmentsInCache": {
"archivedShipmentsInCache": [],
"completedShipmentsInCache": []
},
"includeCurrent": true,
"includeArchived": false,
"languageCode": "de"
}
To retrieve a list of older, archived shipments, set includeCurrent
to false and includeArchived
to true. Setting both values to true resulted in a server error during testing.
Premium Area
The app contains a view, internally referred to as the premium area, that displays at which locations (Packstations or Shops) shipments are ready for pickup.
You can retrieve the information by sending a GET request to https://app.dhl.de/premium-area
.
Customer Information
To retrieve information about the user, send a GET request to https://app.dhl.de/customer-information
. On success, the server will return a 200 response with a JSON object. The object will include, besides others, the email address (email
) and the Postnummer (postNumber
).
mTan
The app also displays the current mTan, which is needed to pick up shipments from the Packstation. Due to fraudulent occurrences in the past (see heise.de), retrieving the mTan requires an additional step in order to verify that the person requesting the mTan has access to the phone, which is used as a second factor.
Step 1: Request mTan verification
The first step is to request verification. A SMS containing a verification code will be sent to the number associated with the account.
To request the SMS, send a POST request to https://app.dhl.de/tan/request
(empty body). Include the headers as described in the "Headers" section. Additionally, the X-Uhash
header is required. It's a simple "hash" that is calculated from the Postnummer of the account (the Postnummer must be specified on all shipments to a packstation and is used to associate shipments with the correct user account). It can be calculated with the following bit of ruby code:
post_number = "07070707"
post_number.unpack("c*").inject(0) { |h, c| ((h * 31) + c) % 10000 }.to_s.rjust(4, "0")
The Postnummer must be specified as an ASCII string. It can optionally be retrieved from the customer-information API endpoint described in section "Customer Information".
On success, the server responds with 204.
Step 2: Perform verification
Once the SMS is received, a GET request must be sent to https://app.dhl.de/tan/token
.
The following header fields are required in addition to the fields described in the section "Headers":
- X-Uhash: refer to the previous step
- X-Targethint: 4
- Tan: Confirmation code recieved via SMS
On success, the server will return a 200 response that contains a JSON object with one attribute: token. It contains a token that is used as an additional authentication factor only when retrieving the current mTan -- the mTan ultimately allows the pickup of shipments from a Packstation.
Step 3: Retrieve mTan
Now we have everything in place to actually retrieve the current mTan:
Just send a POST request to https://app.dhl.de/mTan
.
The body of thre request should have the following format:
{
"generate": false,
"mtan": true
}
Also include the headers previously mentioned as well as an additional header for the token obtained in Step 2:
Token: {TOKEN}
On success, the server will return a 200 response containing a JSON object with the field mTan, which contains the current mTan. If no mTan is available, the server will return a 500 response that includes a JSON object with the field errorText which indicates that no mTan is available (or maybe something else).
Running the examples
The directory examples
contains a bunch of simple ruby scripts demonstrating the usage of the API. They are interactive and will ask for required information. If you have problems pasting in the required values, run the script with the environment variable STTY
set to -icanon
, e.g. STTY=-icanon ruby auth.rb
common.rb
: Defines commonly needed constantsauth.rb
: Performs the authentication procedure and will print out Access and Refresh tokens on successrenew.rb
: Renews the tokens with a Refresh tokenshipments.rb
: Retrieves and prints out (as JSON) the list of current shipmentspremium_area.rb
: Retrieves and prints out the shipments currently ready for pickuptan_enable.rb
: Will perform the verification procedure that results in the issuance of an additional token used for retrieving the current mTantan.rb
: Retrieves the current mTan