44
55namespace SimpleSAML \Module \oidc \Controllers \Api ;
66
7+ use SimpleSAML \Module \oidc \Bridges \SspBridge ;
78use SimpleSAML \Module \oidc \Codebooks \ApiScopesEnum ;
8- use SimpleSAML \Module \oidc \Exceptions \AuthorizationException ;
9+ use SimpleSAML \Module \oidc \Codebooks \ParametersEnum ;
10+ use SimpleSAML \Module \oidc \Entities \ScopeEntity ;
11+ use SimpleSAML \Module \oidc \Entities \UserEntity ;
12+ use SimpleSAML \Module \oidc \Exceptions \OidcException ;
13+ use SimpleSAML \Module \oidc \Factories \Entities \AuthCodeEntityFactory ;
914use SimpleSAML \Module \oidc \Factories \Entities \ClientEntityFactory ;
15+ use SimpleSAML \Module \oidc \Factories \Entities \UserEntityFactory ;
1016use SimpleSAML \Module \oidc \ModuleConfig ;
17+ use SimpleSAML \Module \oidc \Repositories \AuthCodeRepository ;
18+ use SimpleSAML \Module \oidc \Repositories \ClientRepository ;
19+ use SimpleSAML \Module \oidc \Repositories \UserRepository ;
1120use SimpleSAML \Module \oidc \Server \Exceptions \OidcServerException ;
1221use SimpleSAML \Module \oidc \Services \Api \Authorization ;
22+ use SimpleSAML \Module \oidc \Services \LoggerService ;
23+ use SimpleSAML \Module \oidc \Utils \Routes ;
24+ use SimpleSAML \OpenID \Codebooks \ClaimsEnum ;
25+ use SimpleSAML \OpenID \Codebooks \GrantTypesEnum ;
1326use SimpleSAML \OpenID \VerifiableCredentials ;
14- use Symfony \Component \HttpFoundation \JsonResponse ;
1527use Symfony \Component \HttpFoundation \Request ;
1628use Symfony \Component \HttpFoundation \Response ;
1729
@@ -25,30 +37,136 @@ public function __construct(
2537 protected readonly Authorization $ authorization ,
2638 protected readonly VerifiableCredentials $ verifiableCredentials ,
2739 protected readonly ClientEntityFactory $ clientEntityFactory ,
40+ protected readonly ClientRepository $ clientRepository ,
41+ protected readonly SspBridge $ sspBridge ,
42+ protected readonly LoggerService $ loggerService ,
43+ protected readonly UserRepository $ userRepository ,
44+ protected readonly UserEntityFactory $ userEntityFactory ,
45+ protected readonly AuthCodeRepository $ authCodeRepository ,
46+ protected readonly AuthCodeEntityFactory $ authCodeEntityFactory ,
47+ protected readonly Routes $ routes ,
2848 ) {
2949 if (!$ this ->moduleConfig ->getApiEnabled ()) {
3050 throw OidcServerException::forbidden ('API capabilities not enabled. ' );
3151 }
3252 }
3353
3454 /**
35- * @throws AuthorizationException
3655 */
37- public function credentialOffer (Request $ request ): Response
56+ public function preAuthorizedCredentialOffer (Request $ request ): Response
3857 {
3958 $ this ->authorization ->requireTokenForAnyOfScope (
4059 $ request ,
4160 [ApiScopesEnum::VciCredentialOffer, ApiScopesEnum::VciAll, ApiScopesEnum::All],
4261 );
4362
44- $ input = $ request ->getPayload ()->all ();
45-
4663 // Currently, we need a dedicated client for which the PreAuthZed code will be bound to.
47- // TODO mivanci: Remove requirement for dedicated client for authorization codes.
64+ // TODO mivanci: Remove requirement for dedicated client for (pre-) authorization codes.
4865 $ client = $ this ->clientEntityFactory ->getGenericForVciPreAuthZFlow ();
66+ if ($ this ->clientRepository ->findById ($ client ->getIdentifier ()) === null ) {
67+ $ this ->clientRepository ->add ($ client );
68+ } else {
69+ $ this ->clientRepository ->update ($ client );
70+ }
71+
72+ $ input = $ request ->getPayload ()->all ();
73+ $ userAttributes = $ input ['user_attributes ' ] ?? [];
74+
75+ $ selectedCredentialConfigurationId = $ input ['credential_configuration_id ' ] ?? null ;
76+ if ($ selectedCredentialConfigurationId === null ) {
77+ throw new OidcException ('No credential configuration ID provided. ' );
78+ }
79+ $ credentialConfigurationIdsSupported = $ this ->moduleConfig ->getCredentialConfigurationIdsSupported ();
80+
81+ if (empty ($ credentialConfigurationIdsSupported )) {
82+ throw new OidcException ('No credential configuration IDs configured. ' );
83+ }
84+ if (!in_array ($ selectedCredentialConfigurationId , $ credentialConfigurationIdsSupported , true )) {
85+ throw new OidcException (
86+ 'Credential configuration ID not supported: ' . $ selectedCredentialConfigurationId ,
87+ );
88+ }
89+
90+ $ userId = null ;
91+ try {
92+ $ userId = $ this ->sspBridge ->utils ()->attributes ()->getExpectedAttribute (
93+ $ userAttributes ,
94+ $ this ->moduleConfig ->getUserIdentifierAttribute (),
95+ );
96+ } catch (\Throwable $ e ) {
97+ $ this ->loggerService ->warning (
98+ 'Could not extract user identifier from user attributes: ' . $ e ->getMessage (),
99+ );
100+ }
101+
102+ if ($ userId === null ) {
103+ $ sortedAttributes = $ userAttributes ;
104+ $ this ->verifiableCredentials ->helpers ()->arr ()->hybridSort ($ sortedAttributes );
105+ $ userId = 'vci_preauthz_ ' . hash ('sha256 ' , serialize ($ sortedAttributes ));
106+ }
49107
50- dd ( $ this ->verifiableCredentials -> helpers ()-> arr ()-> hybridSort ( $ input [ ' user_attributes ' ]) );
108+ $ oldUserEntity = $ this ->userRepository -> getUserEntityByIdentifier ( $ userId );
51109
52- return new JsonResponse (['ok ' ]);
110+ $ userEntity = $ this ->userEntityFactory ->fromData ($ userId , $ userAttributes );
111+
112+ if ($ oldUserEntity instanceof UserEntity) {
113+ $ this ->userRepository ->update ($ userEntity );
114+ } else {
115+ $ this ->userRepository ->add ($ userEntity );
116+ }
117+
118+
119+ $ authCodeId = $ this ->sspBridge ->utils ()->random ()->generateID ();
120+
121+ if (($ authCode = $ this ->authCodeRepository ->findById ($ authCodeId )) === null ) {
122+ $ authCode = $ this ->authCodeEntityFactory ->fromData (
123+ id: $ authCodeId ,
124+ client: $ client ,
125+ scopes: [
126+ new ScopeEntity ('openid ' ),
127+ new ScopeEntity ($ selectedCredentialConfigurationId ),
128+ ],
129+ expiryDateTime: new \DateTimeImmutable ('+10 minutes ' ),
130+ userIdentifier: $ userId ,
131+ redirectUri: 'openid-credential-offer:// ' ,
132+ );
133+
134+ $ this ->authCodeRepository ->persistNewAuthCode ($ authCode );
135+ }
136+
137+ $ credentialOffer = $ this ->verifiableCredentials ->credentialOfferFactory ()->from (
138+ parameters: [
139+ ClaimsEnum::CredentialIssuer->value => $ this ->moduleConfig ->getIssuer (),
140+ ClaimsEnum::CredentialConfigurationIds->value => [
141+ $ selectedCredentialConfigurationId ,
142+ ],
143+ ClaimsEnum::Grants->value => [
144+ GrantTypesEnum::PreAuthorizedCode->value => [
145+ ClaimsEnum::PreAuthorizedCode->value => $ authCode ->getIdentifier (),
146+ // TODO mivanci support for TxCode
147+ // ClaimsEnum::TxCode->value => [
148+ // ClaimsEnum::InputMode->value => 'numeric',
149+ // ClaimsEnum::Length->value => 6,
150+ // ClaimsEnum::Description->value => 'Sent to user mail',
151+ // ],
152+ ],
153+ ],
154+ ],
155+ );
156+
157+ $ credentialOfferValue = $ credentialOffer ->jsonSerialize ();
158+ $ parameterName = ParametersEnum::CredentialOfferUri->value ;
159+ if (is_array ($ credentialOfferValue )) {
160+ $ parameterName = ParametersEnum::CredentialOffer->value ;
161+ $ credentialOfferValue = json_encode ($ credentialOfferValue );
162+ }
163+
164+ $ credentialOfferUri = "openid-credential-offer://? $ parameterName= $ credentialOfferValue " ;
165+
166+ return $ this ->routes ->newJsonResponse (
167+ data: [
168+ 'credential_offer_uri ' => $ credentialOfferUri ,
169+ ],
170+ );
53171 }
54172}
0 commit comments