-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into feature/ILF
- Loading branch information
Showing
93 changed files
with
103,320 additions
and
1,149 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,354 @@ | ||
= FHIR VZD HOWTO Authenticate | ||
:source-highlighter: rouge | ||
:icons: | ||
:title-page: | ||
:imagesdir: /images/ | ||
ifdef::env-github[] | ||
:toc: preamble | ||
endif::[] | ||
ifndef::env-github[] | ||
:toc: left | ||
endif::[] | ||
:toclevels: 3 | ||
:toc-title: Table of Contents | ||
:sectnums: | ||
|
||
|
||
image::gematik_logo.svg[gematik,float="right"] | ||
|
||
[width="100%",cols="50%,50%",options="header",] | ||
|=== | ||
|Version: |1.0.2 | ||
|Referencing: |gemILF_FHIR_VZD | ||
|=== | ||
|
||
[big]*Document history* | ||
|
||
[width="100%",cols="11%,11%,7%,58%,13%",options="header",] | ||
|=== | ||
|*Version* + | ||
|*Stand* + | ||
|*Chap./ Page* + | ||
|*Change reason, special instructions* + | ||
|*Editing* + | ||
|
||
|1.0.0 |28.07.23 | |Initial document |gematik | ||
|
||
|1.0.1 |25.08.23 |Chap. 2.4 |added chapter for owner auth |gematik | ||
|
||
|1.0.2 |11.09.23 |Chap. 2.5 |added chapter "2.5. Authenticate using the gematik Authenticator" |gematik | ||
|
||
|=== | ||
|
||
== Classification of the document | ||
=== Objective | ||
This document describes how the different authentication interfaces of the FHIR directory service can be used to obtain access_tokens. | ||
|
||
=== Target group | ||
|
||
The document is aimed at software developers who are involved in implementing a client of the FHIR directory service and need access_tokens to interact with the API. | ||
|
||
=== Scope | ||
|
||
*Intellectual property/patent notice* | ||
|
||
_The following specification was created by gematik solely from a technical point of view. In individual cases, it cannot be ruled out that the implementation of the specification will interfere with the technical property rights of third parties. It is solely up to the supplier or manufacturer to take suitable measures to ensure that the products and/or services offered by him on the basis of the specification do not infringe third-party property rights and, if necessary, to obtain the necessary permits/licenses from the property right holders concerned. In this respect, gematik GmbH assumes no liability whatsoever._ | ||
|
||
|
||
== FHIRDirectoryAuthenticationAPIs | ||
=== Authenticate for the search endpoint | ||
To use the search endpoint, a corresponding access token from the FHIRDirectoryAuthorizationService must be available. + | ||
+ | ||
With the following procedure an access token can be obtained: | ||
|
||
*Get authentication methods supported by the Matrix homeserver* | ||
[source] | ||
---- | ||
curl -X GET "[Matrix homeserver url]/_matrix/client/v3/login" | ||
---- | ||
The request returns the supported authentication methods | ||
[source] | ||
---- | ||
200 | ||
{ | ||
"flows": [ | ||
{ | ||
"type": "m.login.password" | ||
}, | ||
{ | ||
"type": "m.login.application_service" | ||
}, | ||
{ | ||
"type": "uk.half-shot.msc2778.login.application_service" | ||
} | ||
] | ||
} | ||
---- | ||
|
||
*Perform login and get Matrix Access Token* | ||
|
||
The login operation depends from the selected authentication methods which can be dependent from the used environment. | ||
|
||
[source] | ||
---- | ||
curl -X POST "[Matrix homeserver url]/_matrix/client/v3/login" \ | ||
-H "Content-Type: application/json" \ | ||
-d @- << EOF | ||
{ | ||
"type": "m.login.password", | ||
"user": "username", | ||
"password": "password of the user" | ||
} | ||
EOF | ||
---- | ||
The request returns a matrix access_token | ||
[source] | ||
---- | ||
200 | ||
{ | ||
"user_id": "username", | ||
"access_token": "syt_ZXR0cjAy_JVJehocKZFlYyRbtTdiz_18uj52", | ||
"home_server": "matix.home.server.de", | ||
"device_id": "ABCDEFGHIJ" | ||
} | ||
---- | ||
|
||
.Search-Acccesstoken Flow | ||
[%collapsible%open] | ||
==== | ||
++++ | ||
<p align="center"> | ||
<img width="55%" src=../images/diagrams/SequenceDiagram.FHIR-Directory.search_auth.svg> | ||
</p> | ||
++++ | ||
==== | ||
|
||
*[01] Request a Matrix OpenID Token* | ||
[source] | ||
---- | ||
curl -X POST "[Matrix homeserver url]/_matrix/client/v3/user/{user_id}/openid/request_token?access_token=syt_ZXR0cjAy_JVJehocKZFlYyRbtTdiz_18uj52" \ | ||
-H "Content-Type: application/json" \ | ||
-d @- << EOF | ||
{} | ||
EOF | ||
---- | ||
In this operation the "username" has to be replaced with the user_id received in the response above. + | ||
For the access_token parameter the access_token returned in the response above has to be used. + | ||
+ | ||
*[02] The request returns a matrix access_token* | ||
[source] | ||
---- | ||
200 | ||
{ | ||
"access_token": "zbKsYFDoZBWcKJMylLjCcMcR", | ||
"token_type": "Bearer", | ||
"matrix_server_name": "matix.home.server.de", | ||
"expires_in": 3600 | ||
} | ||
---- | ||
|
||
*[03] Get the access token from the FHIR-Directory* | ||
[source] | ||
---- | ||
curl -X GET "[FHIRbaseUrl]/tim-authenticate?mxId=matrix.dev.service-ti.de" \ | ||
-H "X-Matrix-OpenID-Token: zbKsYFDoZBWcKJMylLjCcMcR" | ||
---- | ||
For the X-Matrix-OpenID-Token parameter the access_token aquired by the "Request Matrix OpenID Token" operation has to be used. + | ||
+ | ||
*[08] The request returns a access token from the FHIR-Directory* | ||
[source] | ||
---- | ||
200 | ||
{ | ||
"access_token": | ||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.e2lzcyI6Imh0dHBzOi8vZmhpci1kaXJlY3RvcnktdGVzdC52emQudGktZGllbnN0ZS5kZS90aW0tYXV0aGVudGljYXRlIiwiYXVkIjoiaHR0cHM6Ly9maGlyLWRpcmVjdG9yeS10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlL3NlYWNoIiwic3ViIjoiQGFiY2RlOm1hdHJpeC5kZXYuc2VydmljZS10aS5kZSIsImlhdCI6MTY2NDEyMjY3MywiZXhwIjoxNjY0MjA5MDczfQ.CoTwrZmZJyfVYVJFD068QJNFo0YLemhfPVER_lW5h3MU2hgoiSj1lkD6yDHPDQAs4JJ6PlBWIUHtoGoYAwVOVw", | ||
"token_type": "bearer", | ||
"expires_in": 86400 | ||
} | ||
---- | ||
|
||
TIP: Please note, that this example access token has an invalid signature (sub claim anonymized). | ||
|
||
The access token is a JWT with the following structure: | ||
|
||
[source] | ||
---- | ||
Header: | ||
{ | ||
"typ": "JWT", | ||
"alg": "ES256" | ||
} | ||
Payload: | ||
{ | ||
"iss": "https://fhir-directory-test.vzd.ti-dienste.de/tim-authenticate\", | ||
"aud": "https://fhir-directory-test.vzd.ti-dienste.de/seach", | ||
"sub": "@abcde:matrix.dev.service-ti.de", | ||
"iat": 1664122673, | ||
"exp": 1664209073 | ||
} | ||
Signature: | ||
... | ||
---- | ||
|
||
For search examples see: link:FHIR_VZD_HOWTO_Search.adoc[Search Examples] | ||
|
||
=== Authenticate for the provider API | ||
To use the TIM-PROVIDER-API, a corresponding TIM-Provider-API Token is needed. | ||
The following sequence diagram shows the flow to obtain such a token | ||
|
||
.Search-Acccesstoken Flow | ||
[%collapsible%open] | ||
==== | ||
++++ | ||
<p align="center"> | ||
<img width="55%" src=../images/diagrams/SequenceDiagram.FHIR-Directory.tim_provider_auth.svg> | ||
</p> | ||
++++ | ||
==== | ||
|
||
*[01] With the following request a ti-provider-accesstoken can be obtained:* + | ||
(The Authorization string contains the base64 encoded client credentials.) | ||
|
||
[source] | ||
---- | ||
curl -X POST "[baseUrl]:9443/auth/realms/TI-Provider/protocol/openid-connect/token" \ | ||
-H "Content-Type: application/x-www-form-urlencoded" \ | ||
-H "Authorization: Basic VElNUHJvdmlkZXI6YjEyMzMxZTktYWJjZC00NTY3LTZhaWUtZGY4ZTY4Y2E0ZmZj=" \ | ||
-d @- << EOF | ||
grant_type=client_credentials | ||
EOF | ||
---- | ||
|
||
|
||
*[03] The request returns a ti-provider-accesstoken.* | ||
[source] | ||
---- | ||
{ | ||
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiMkJXT0w2dmN2QjluMjhnYVRwQ19fQllnXzI1aDljMkN2UGVXbmFGYUVNIn0.eyJqdGkiOiI5YjFkYjQ2ZC0yOThmLTQ0ZGItOTQ3ZC0wODdmYTNkYjQ2YzMiLCJleHAiOjE2Nzg4NjcwMjQsIm5iZiI6MCwiaWF0IjoxNjc4ODY2NzI0LCJpc3MiOiJodHRwOi8vYXV0aC10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlOjk0NDMvYXV0aC9yZWFsbXMvVEktUHJvdmlkZXIiLCJzdWIiOiI2MWJjODJiOC0yN2VlLTRkMDUtYTc0OC1iNmQ3Zjc4NDk4ZmIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJnZW1hdGlrIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMjg4NTc4MDgtNWIyNS00NWJhLWIzMDEtOGVkYjQzNTA4Mzc4IiwiYWNyIjoiMSIsInNjb3BlIjoidGktcHJvdmlkZXIiLCJjbGllbnRJZCI6ImdlbWF0aWsiLCJjbGllbnRIb3N0IjoiMTcyLjI0LjQ2LjEwIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yNC40Ni4xMCJ9.LUbOr4XlICADdYuyT_SgviTBdXkSKdXQ43Fnp9VF7lr71Zrf0ELWiXkr1SplrMUmJrrbff8SaEzZTyy9Fx32xFTJnkX_Dw3uugbWXcARC-895lotzJ1Je4CJZSNSUSv4m6e86M8L7dfXjXxrbPAh4hH8IFWpudgF0mPG12VQMhaupJu13o2gIzB1GL7dk567_RVWXML8hi4ib6DT5dgZjIHFKKSAeuw99Xq0m1XdkwPCL9t8aIemY9lShQ2obvj70C8jBaIjquNGAScIrn3oTTLXs54XBN-t839M5wU0fxpwwHhuaDpRF-uLmPdQA3zE7MCNJYXkzl9nWdh2dLD31w", | ||
"expires_in":300, | ||
"refresh_expires_in":1800, | ||
"refresh_token": | ||
"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwMjk2NzMzNC1hOGJhLTQyNWYtYWQyOC1jYzAyOWE4YThhZDAifQ.eyJqdGkiOiIxMGY4ZjAyNi1iMzA1LTRjYmMtOWU4Yi1hZWE5MWY0YTRiMTciLCJleHAiOjE2Nzg4Njg1MjQsIm5iZiI6MCwiaWF0IjoxNjc4ODY2NzI0LCJpc3MiOiJodHRwOi8vYXV0aC10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlOjk0NDMvYXV0aC9yZWFsbXMvVEktUHJvdmlkZXIiLCJhdWQiOiJodHRwOi8vYXV0aC10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlOjk0NDMvYXV0aC9yZWFsbXMvVEktUHJvdmlkZXIiLCJzdWIiOiI2MWJjODJiOC0yN2VlLTRkMDUtYTc0OC1iNmQ3Zjc4NDk4ZmIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiZ2VtYXRpayIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjI4ODU3ODA4LTViMjUtNDViYS1iMzAxLThlZGI0MzUwODM3OCIsInNjb3BlIjoidGktcHJvdmlkZXIifQ.FlfHbsFoC26fcwVzsQ4-m_D7ZaKigWvP7We-X5IlBaA", | ||
"token_type":"bearer", | ||
"not-before-policy":0, | ||
"session_state":"28857808-5b25-45ba-b301-8edb43508378", | ||
"scope":"ti-provider" | ||
} | ||
---- | ||
|
||
*[04] Exchange the ti-provider-accesstoken for the tim-providerAPI-accesstoken* | ||
[source] | ||
---- | ||
curl -X GET "[baseUrl]/ti-provider-authenticate" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiMkJXT0w2dmN2QjluMjhnYVRwQ19fQllnXzI1aDljMkN2UGVXbmFGYUVNIn0.eyJqdGkiOiI5YjFkYjQ2ZC0yOThmLTQ0ZGItOTQ3ZC0wODdmYTNkYjQ2YzMiLCJleHAiOjE2Nzg4NjcwMjQsIm5iZiI6MCwiaWF0IjoxNjc4ODY2NzI0LCJpc3MiOiJodHRwOi8vYXV0aC10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlOjk0NDMvYXV0aC9yZWFsbXMvVEktUHJvdmlkZXIiLCJzdWIiOiI2MWJjODJiOC0yN2VlLTRkMDUtYTc0OC1iNmQ3Zjc4NDk4ZmIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJnZW1hdGlrIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMjg4NTc4MDgtNWIyNS00NWJhLWIzMDEtOGVkYjQzNTA4Mzc4IiwiYWNyIjoiMSIsInNjb3BlIjoidGktcHJvdmlkZXIiLCJjbGllbnRJZCI6ImdlbWF0aWsiLCJjbGllbnRIb3N0IjoiMTcyLjI0LjQ2LjEwIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yNC40Ni4xMCJ9.LUbOr4XlICADdYuyT_SgviTBdXkSKdXQ43Fnp9VF7lr71Zrf0ELWiXkr1SplrMUmJrrbff8SaEzZTyy9Fx32xFTJnkX_Dw3uugbWXcARC-895lotzJ1Je4CJZSNSUSv4m6e86M8L7dfXjXxrbPAh4hH8IFWpudgF0mPG12VQMhaupJu13o2gIzB1GL7dk567_RVWXML8hi4ib6DT5dgZjIHFKKSAeuw99Xq0m1XdkwPCL9t8aIemY9lShQ2obvj70C8jBaIjquNGAScIrn3oTTLXs54XBN-t839M5wU0fxpwwHhuaDpRF-uLmPdQA3zE7MCNJYXkzl9nWdh2dLD31w" | ||
---- | ||
|
||
*[06] This request returns the tim-providerAPI-accesstoken.* | ||
[source] | ||
---- | ||
{ | ||
"access_token": | ||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJodHRwczovL2ZoaXItZGlyZWN0b3J5LXRlc3QudnpkLnRpLWRpZW5zdGUuZGUvdGktcHJvdmlkZXItYXV0aGVudGljYXRlIiwiYXVkIjoiaHR0cHM6Ly9maGlyLWRpcmVjdG9yeS10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlL3RpbS1wcm92aWRlci1zZXJ2aWNlcyIsInN1YiI6ImdlbWF0aWsiLCJpYXQiOjE2Nzg4NzI5NjAsImV4cCI6MTY3ODk1OTM2MCwiY2xpZW50SWQiOiJnZW1hdGlrIn0.hZDJXa-l1OK_B2NE54znl5FYWa5mW01Fw7LOiDo2kNIJOx2HPDCnEqxixd-7m9L34wq8WE4qVaA9jB6BNwX7qw", | ||
"client_id":"gematik", | ||
"token_type":"bearer", | ||
"expires_in":86400 | ||
} | ||
---- | ||
|
||
For provider api usage examples see: link:FHIR_VZD_HOWTO_Provider.adoc[Provider Examples] | ||
|
||
=== Authenticate for the owner endpoint as an organization | ||
To use the API operations, a corresponding TI-Provider API Token from the FHIRDirectoryAuthorizationService must be available. + | ||
+ | ||
With the following operation an access token can be obtained: | ||
|
||
==== Authenticate with an RegService-OpenID-Token | ||
One way to authenticate an organization is the possibility to exchange an RegService-OpenID-Token for an owneraccess-token. | ||
|
||
The RegService-OpenID-Token must have the following structure: | ||
|
||
.RegService-OpenID-Token structure | ||
[%collapsible%closed] | ||
==== | ||
[source, json] | ||
---- | ||
HEADER | ||
{ | ||
"alg": "BP256R1", | ||
"typ": "JWT" | ||
"x5c": [ | ||
"<X.509 Sig-Cert, base64-encoded DER>" | ||
] | ||
} | ||
PAYLOAD | ||
{ | ||
"sub": "1234567890", | ||
"iss": "<url des Registrierungs-Dienst-Endpunkts, über den das Token ausgestellt wurde>", | ||
"aud": "https://vzd-fhir-directory.vzd.ti-dienste.de/owner-authenticate", | ||
"professionOID": "<professionOID der Organisation>", | ||
"idNummer": "<telematikID der Organisation>", | ||
"iat": "1516239022", | ||
"exp": "1516239022" | ||
} | ||
---- | ||
==== | ||
|
||
The RegService-OpenID-Token can be exchanged for an owneraccess-token | ||
[source] | ||
---- | ||
curl -X GET "[baseUrl]/owner-authenticate" -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiMkJXT0w2dmN2QjluMjhnYVRwQ19fQllnXzI1aDljMkN2UGVXbmFGYUVNIn0.eyJqdGkiOiI5YjFkYjQ2ZC0yOThmLTQ0ZGItOTQ3ZC0wODdmYTNkYjQ2YzMiLCJleHAiOjE2Nzg4NjcwMjQsIm5iZiI6MCwiaWF0IjoxNjc4ODY2NzI0LCJpc3MiOiJodHRwOi8vYXV0aC10ZXN0LnZ6ZC50aS1kaWVuc3RlLmRlOjk0NDMvYXV0aC9yZWFsbXMvVEktUHJvdmlkZXIiLCJzdWIiOiI2MWJjODJiOC0yN2VlLTRkMDUtYTc0OC1iNmQ3Zjc4NDk4ZmIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJnZW1hdGlrIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiMjg4NTc4MDgtNWIyNS00NWJhLWIzMDEtOGVkYjQzNTA4Mzc4IiwiYWNyIjoiMSIsInNjb3BlIjoidGktcHJvdmlkZXIiLCJjbGllbnRJZCI6ImdlbWF0aWsiLCJjbGllbnRIb3N0IjoiMTcyLjI0LjQ2LjEwIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yNC40Ni4xMCJ9.LUbOr4XlICADdYuyT_SgviTBdXkSKdXQ43Fnp9VF7lr71Zrf0ELWiXkr1SplrMUmJrrbff8SaEzZTyy9Fx32xFTJnkX_Dw3uugbWXcARC-895lotzJ1Je4CJZSNSUSv4m6e86M8L7dfXjXxrbPAh4hH8IFWpudgF0mPG12VQMhaupJu13o2gIzB1GL7dk567_RVWXML8hi4ib6DT5dgZjIHFKKSAeuw99Xq0m1XdkwPCL9t8aIemY9lShQ2obvj70C8jBaIjquNGAScIrn3oTTLXs54XBN-t839M5wU0fxpwwHhuaDpRF-uLmPdQA3zE7MCNJYXkzl9nWdh2dLD31w" | ||
---- | ||
|
||
.RegService-OpenID-Token validation steps | ||
[%collapsible%open] | ||
==== | ||
. jwt structure must follow https://www.rfc-editor.org/rfc/rfc7519#section-7.2[RFC7519 Validating a JWT] | ||
. validating certificate | ||
.. type is C.FD.SIG | ||
.. technical role is "oid_tim" | ||
. validating the used algorithm(BP256R1) and the expiration time | ||
. checking for existence of field idnummer containing the TelematikID | ||
*Optional and mandatory starting with FHIR VZD 1.2* | ||
. compare the signature certificate with the one handed over while registering for the provider services endpoint credentials | ||
. OCSP check of the signature certificate | ||
. validating signature certificate against the X.509-Root-CA certificate | ||
==== | ||
|
||
=== Authenticate for the owner endpoint as an user | ||
The user can authenticate himself by using his smartcard(HBA/SMC-B). The following diagram shows the interaction between a client, the Auth-Service of the VZD-FHIR Directory, a smartcard and the IDP. | ||
|
||
CAUTION: The diagramm displays the most important interaction parts. For detailed information on the checks performed by the IDP or the detailed smartcard interaction steps please consult the product specific specification. | ||
|
||
.owner-authenticate Flow | ||
[%collapsible%open] | ||
==== | ||
++++ | ||
<p align="center"> | ||
<img width="55%" src=../images/diagrams/SequenceDiagram.FHIR-Directory.owner_auth.svg> | ||
</p> | ||
++++ | ||
==== | ||
|
||
=== Authenticate using the gematik Authenticator | ||
The link:https://fachportal.gematik.de/hersteller-anbieter/komponenten-dienste/authenticator[gematik Authenticator] is a windows application that can be used to simplify the interaction with a connector, the card terminals and the users smartcards(SMC-B/HBA). The authenticator app can be started using a deeplink call (authenticator://) and the authenticator app takes over control. The authenticator offers 2 possibilities to interact with the caller: | ||
|
||
* calling the redirect_uri using the registered application for http calls | ||
* calling the redirect_uri directly using a HTTP get Request | ||
|
||
The first option would require that the redirect_uri provided by the VZD-FHIR directory has to return application specific content that fits the callers needs. As the caller can be any application, this flow is not an option. | ||
For the second option the caller needs information on the actual status of the authenticator authorization process.To fullfill this need the VZD-FHIR directory will provide an endpoint that can be used by clients to query the actual authorization process status. | ||
The following sequence diagramm shows the process in detail. | ||
|
||
.owner-authenticate with the gematik Authenticator | ||
[%collapsible%open] | ||
==== | ||
++++ | ||
<p align="center"> | ||
<img width="55%" src=../images/diagrams/SequenceDiagram.FHIR-Directory.owner_auth_authenticator.svg> | ||
</p> | ||
++++ | ||
==== |
Oops, something went wrong.