1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace SimpleSAML \Module \oidc \Factories ;
6+
7+ use DateTimeImmutable ;
8+ use RuntimeException ;
9+ use SimpleSAML \Module \oidc \Bridges \SspBridge ;
10+ use SimpleSAML \Module \oidc \Codebooks \ParametersEnum ;
11+ use SimpleSAML \Module \oidc \Entities \ScopeEntity ;
12+ use SimpleSAML \Module \oidc \Entities \UserEntity ;
13+ use SimpleSAML \Module \oidc \Exceptions \OidcException ;
14+ use SimpleSAML \Module \oidc \Factories \Entities \AuthCodeEntityFactory ;
15+ use SimpleSAML \Module \oidc \Factories \Entities \ClientEntityFactory ;
16+ use SimpleSAML \Module \oidc \Factories \Entities \UserEntityFactory ;
17+ use SimpleSAML \Module \oidc \ModuleConfig ;
18+ use SimpleSAML \Module \oidc \Repositories \AuthCodeRepository ;
19+ use SimpleSAML \Module \oidc \Repositories \ClientRepository ;
20+ use SimpleSAML \Module \oidc \Repositories \UserRepository ;
21+ use SimpleSAML \Module \oidc \Services \LoggerService ;
22+ use SimpleSAML \OpenID \Codebooks \ClaimsEnum ;
23+ use SimpleSAML \OpenID \Codebooks \GrantTypesEnum ;
24+ use SimpleSAML \OpenID \VerifiableCredentials ;
25+
26+ class CredentialOfferUriFactory
27+ {
28+ public function __construct (
29+ protected readonly VerifiableCredentials $ verifiableCredentials ,
30+ protected readonly ModuleConfig $ moduleConfig ,
31+ protected readonly SspBridge $ sspBridge ,
32+ protected readonly AuthCodeRepository $ authCodeRepository ,
33+ protected readonly AuthCodeEntityFactory $ authCodeEntityFactory ,
34+ protected readonly ClientEntityFactory $ clientEntityFactory ,
35+ protected readonly ClientRepository $ clientRepository ,
36+ protected readonly LoggerService $ loggerService ,
37+ protected readonly UserRepository $ userRepository ,
38+ protected readonly UserEntityFactory $ userEntityFactory ,
39+ )
40+ {
41+ }
42+
43+ /**
44+ * @param string[] $credentialConfigurationIds
45+ * @throws OidcException
46+ */
47+ public function buildPreAuthorized (
48+ array $ credentialConfigurationIds ,
49+ array $ userAttributes ,
50+ ): string
51+ {
52+ if (empty ($ credentialConfigurationIds )) {
53+ throw new RuntimeException ('No credential configuration IDs provided. ' );
54+ }
55+
56+ $ credentialConfigurationIdsSupported = $ this ->moduleConfig ->getCredentialConfigurationIdsSupported ();
57+
58+ if (empty ($ credentialConfigurationIdsSupported )) {
59+ throw new RuntimeException ('No credential configuration IDs configured. ' );
60+ }
61+
62+ if (array_diff ($ credentialConfigurationIds , $ credentialConfigurationIdsSupported )) {
63+ throw new RuntimeException ('Unsupported credential configuration IDs provided. ' );
64+ }
65+
66+ /* TODO mivanci TX Code handling
67+ $email = $this->emailFactory->build(
68+ subject: 'VC Issuance Transaction code',
69+ 70+ );
71+
72+ $email->setData(['Transaction Code' => '1234']);
73+ try {
74+ $email->send();
75+ $this->sessionMessagesService->addMessage('Email with tx code sent to: [email protected] '); 76+ } catch (Exception $e) {
77+ $this->sessionMessagesService->addMessage('Error emailing tx code.');
78+ }
79+ */
80+
81+ // TODO mivanci Wallet (client) credential_offer_endpoint metadata
82+ // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#client-metadata
83+
84+ $ scopes = array_map (
85+ fn (string $ scope ) => new ScopeEntity ($ scope ),
86+ ['openid ' , ...$ credentialConfigurationIds ],
87+ );
88+
89+ // Currently, we need a dedicated client for which the PreAuthZed code will be bound to.
90+ // TODO mivanci: Remove requirement for dedicated client for (pre-)authorization codes.
91+ $ client = $ this ->clientEntityFactory ->getGenericForVciPreAuthZFlow ();
92+ if ($ this ->clientRepository ->findById ($ client ->getIdentifier ()) === null ) {
93+ $ this ->clientRepository ->add ($ client );
94+ } else {
95+ $ this ->clientRepository ->update ($ client );
96+ }
97+
98+ $ userId = null ;
99+ try {
100+ $ userId = $ this ->sspBridge ->utils ()->attributes ()->getExpectedAttribute (
101+ $ userAttributes ,
102+ $ this ->moduleConfig ->getUserIdentifierAttribute (),
103+ );
104+ } catch (\Throwable $ e ) {
105+ $ this ->loggerService ->warning (
106+ 'Could not extract user identifier from user attributes: ' . $ e ->getMessage (),
107+ );
108+ }
109+
110+ if ($ userId === null ) {
111+ $ sortedAttributes = $ userAttributes ;
112+ $ this ->verifiableCredentials ->helpers ()->arr ()->hybridSort ($ sortedAttributes );
113+ $ userId = 'vci_preauthz_ ' . hash ('sha256 ' , serialize ($ sortedAttributes ));
114+ }
115+
116+ $ oldUserEntity = $ this ->userRepository ->getUserEntityByIdentifier ($ userId );
117+
118+ $ userEntity = $ this ->userEntityFactory ->fromData ($ userId , $ userAttributes );
119+
120+ if ($ oldUserEntity instanceof UserEntity) {
121+ $ this ->userRepository ->update ($ userEntity );
122+ } else {
123+ $ this ->userRepository ->add ($ userEntity );
124+ }
125+
126+ $ authCodeId = null ;
127+ $ authCodeIdGenerationAttempts = 3 ;
128+ while ($ authCodeIdGenerationAttempts > 0 ) {
129+ $ authCodeId = $ this ->sspBridge ->utils ()->random ()->generateID ();
130+ if ($ this ->authCodeRepository ->findById ($ authCodeId ) === null ) {
131+ break ;
132+ }
133+ $ authCodeIdGenerationAttempts --;
134+ }
135+
136+ if ($ authCodeId === null ) {
137+ throw new RuntimeException ('Failed to generate Authorization Code ID. ' );
138+ }
139+
140+ // TODO mivanci Add indication of preAuthZ code to the auth code table.
141+ $ authCode = $ this ->authCodeEntityFactory ->fromData (
142+ id: $ authCodeId ,
143+ client: $ client ,
144+ scopes: $ scopes ,
145+ expiryDateTime: (new DateTimeImmutable ())->add ($ this ->moduleConfig ->getAuthCodeDuration ()),
146+ userIdentifier: $ userId ,
147+ redirectUri: 'openid-credential-offer:// ' ,
148+ );
149+ $ this ->authCodeRepository ->persistNewAuthCode ($ authCode );
150+
151+ $ credentialOffer = $ this ->verifiableCredentials ->credentialOfferFactory ()->from (
152+ parameters: [
153+ ClaimsEnum::CredentialIssuer->value => $ this ->moduleConfig ->getIssuer (),
154+ ClaimsEnum::CredentialConfigurationIds->value => [
155+ ...$ credentialConfigurationIds ,
156+ ],
157+ ClaimsEnum::Grants->value => [
158+ GrantTypesEnum::PreAuthorizedCode->value => [
159+ ClaimsEnum::PreAuthorizedCode->value => $ authCode ->getIdentifier (),
160+ // TODO mivanci support for TxCode
161+ // ClaimsEnum::TxCode->value => [
162+ // ClaimsEnum::InputMode->value => 'numeric',
163+ // ClaimsEnum::Length->value => 6,
164+ // ClaimsEnum::Description->value => 'Sent to user mail',
165+ // ],
166+ ],
167+ ],
168+ ],
169+ );
170+
171+ $ credentialOfferValue = $ credentialOffer ->jsonSerialize ();
172+ $ parameterName = ParametersEnum::CredentialOfferUri->value ;
173+ if (is_array ($ credentialOfferValue )) {
174+ $ parameterName = ParametersEnum::CredentialOffer->value ;
175+ $ credentialOfferValue = json_encode ($ credentialOfferValue );
176+ }
177+
178+ return "openid-credential-offer://? $ parameterName= $ credentialOfferValue " ;
179+ }
180+ }
0 commit comments