From ca93627b274e367943f029aa876b498597bc478a Mon Sep 17 00:00:00 2001 From: Mykhailo Sizov Date: Fri, 14 Jul 2023 12:24:51 +0300 Subject: [PATCH 01/22] feat: OIDC4VCI Authorization code flow - Support for Wallet Initiated flow Signed-off-by: Mykhailo Sizov --- api/spec/openapi.gen.go | 270 +++++++++--------- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 187 ++++++++++++ docs/v1/openapi.yaml | 61 ++++ pkg/profile/api.go | 1 + pkg/restapi/v1/issuer/controller.go | 47 ++- pkg/restapi/v1/issuer/openapi.gen.go | 204 +++++++++++++ pkg/restapi/v1/oidc4ci/controller.go | 91 +++++- pkg/service/oidc4ci/api.go | 37 ++- pkg/service/oidc4ci/oidc4ci_service.go | 212 +++++++++++--- .../oidc4ci/oidc4ci_service_exchange_code.go | 10 +- .../oidc4ci_service_initiate_issuance.go | 37 +-- .../oidc4ci_service_store_auth_code.go | 6 +- .../oidc4cinoncestore/oidc4vc_store.go | 3 + test/bdd/bddtests_test.go | 2 +- test/bdd/features/oidc4vc_api.feature | 214 +++++++------- test/bdd/fixtures/oauth-clients/clients.json | 6 +- test/bdd/fixtures/profile/profiles.json | 3 +- test/bdd/pkg/v1/oidc4vc/oidc4ci.go | 24 ++ test/bdd/pkg/v1/oidc4vc/steps.go | 2 +- 19 files changed, 1090 insertions(+), 327 deletions(-) diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 86c63fe0e..790872b23 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,139 +19,143 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+w9/XPbtpL/CkZ3M0lmZDuvH+/d8/1yrpX2qU1iP39lbpqMBiIhCQ1FsABoRc34f7/B", - "AiABEiAp20rbq39qYxHAYr+wu1jsfh4lbF2wnORSjI4/j0SyImsM/3uSJESIK/aR5BdEFCwXRP05JSLh", - "tJCU5aPj0RuWkgwtGEf6cwTfIzvgcDQeFZwVhEtKYFYMn82k+qw93dWKIP0Fgi8QFaIkKZpvkVQ/lXLF", - "OP0Nq8+RIPyWcLWE3BZkdDwSktN8Obobj5JZzvIkAO8lfIISlktMc/W/GMGnSDI0J6gUJFX/m3CCJUEY", - "FZyxBWILVDAhiBBqYbZAH8kWrbEknOIMbVYkR5z8WhIh9ZQJJynJJcVZF3gz8qmgnIgZDaBimkuyJByl", - "JGcwq0JARhdE0jVBVG0/YXkqFDTqJzOnsx7VM6gFuxa66p7XJUd4ck4WnIhVF03NJ3qWMdqsaLJCCc5d", - "lLO5IgnKycZbUwQxKBJWBMh7dn41PXt78nqM6AJRIEGCMzW72goMsoSquSrJKMnlfyMmV4RvqCBjdPHq", - "39fTi1eT4NoA1kz/ObRZ9YvFnsvFgckAe7+WlJN0dPyzLxzeQh/GI0llpsaG5LKamM1/IYkcjUefDiRe", - "CjUpo2nyTUJHH+7Go9OKLydUFBneqh34ApqxBGews9bGc7wO/XBXw9aePwKZAgywwhtwXWjqdGmas+nk", - "FNUjLEHbumbB+BoHpvoe/l4JTj3TnChBi1IL5mcLNeF/crIYHY/+46hWn0dGdx79+O7qHL670zOINgQn", - "nOMtAKB+HwAJlWQtgkQxf8BqxhZD6eU/hAhkEb076ww5Ddo06joQComVOowokBP04+XZWyQCqluLlyjn", - "Qu0ml9m2qU6wA8UhenN9eaV0TsGJILnUmttBOxUoZxJxIkueR3ggerZEodzDAXP68AMGwKWPecrUiFSr", - "sZycLUbHP7d59nOD5e4Uf8WE1cWqB+XCk2JjKHTipSEcZkUP7oio3FvNXkosy4ACcERDwCdtwRDV0IjM", - "f+7Zn5nAfB7c2aX3SXBfQS2tx50VAXqdwf8I0AJqLEiDRxV/m8P20rcFBcrAXbz6lKxwviQnrjF5ylIy", - "4OgheizIYClXKGEpQQvO1pr/OGLqz609smKmiDFgn9WXzl57AX74xmMK3f6C1g9Fgfw0o+kAOsNnwzY/", - "QCid3U9zKimWRJ1M35xOBxDbjmgdZlMhSnVgoYuY5eE5KrOUSEyzkBYohWRr+hsRaLPCEn2keaoUmrF/", - "pxqhG5xLZQyjJb2FY+Tm9DKs9TNM17MUSxyipEYy7OyckwOLUKU0FQm/z9jmUE2tt3tJ+C1NlPkvBcIC", - "nZ3DyA3OMiIRLoqMJrC7tvaoICF5WjCaB5B8qn5H9nerw81+gZk2K8I9iwimRGpzaIWFOU1rWx4vJOFI", - "lIC5RZllW4QTtWVg1F5/QvsAM2pIPqOGxLOSZ23wry9eu+cO8IIZqo5vd18YvQOUHaIr/JEIZXkkak8J", - "QeyWcON8zDYkyz7mbFMd86jAHK+JJPwQTRdozpSodQCJcJ62J8OcgEFTcHZLU2V5aEvCSLWdqd6F2tmG", - "Zpk1YFACLBr5kubVKVyQnKYH9rMD+9nx0VEXvitIh3jqmveOVixLCXdZUHOsnhLVm09YvqDLkutvri9e", - "hyGpWGzmAdBxYrs/dM9o7a+QYTNR6GwYoAKJFSuzVPF2wnJBYacC6XnSUW0mjVKFZmWl9YBgnbbobuCD", - "7jkkWRcZcFwacHnNjwEvRgupMc02K5oRX0ITlidZmWqLjgqwRjlO1MSHlQ8OvryauOBsoaagoiKttqVL", - "dUCVmaRF5i9vIAuL/JLjXEbceKOJEpxb0bGCAKOM2yZXnJXLlYbdkdcr9e/6Q0dfgfWvEeGeo7kf9FKK", - "1g91wSFLc6R2w5GQpBCgFtqynZIFLjOp1vMPITVFEA+ucRIUwVuclcQ4LFXQpHEcKjZVZ1eBfy2Jjbdo", - "zYekOtqoqFyfuTrlIB5Tzg+M4wbA6nANbNhqwQ2Vq8h6aoegHsgniQSRqCxQWgLEBSe3lJXCwVQd6EFK", - "A9NbIhA2W1P49mk4RlRqZ5EChxL1b5pbqC3QJz7Qxhyw2w+gSMAPFuP1ehoQ45++PbuqeIXmyLN89Fm9", - "yNhGq46CkwNcneQzzSfC+rdBelvtH2H9U61wRX1KAA8bIsI2yKeCKLNAGQtG/DRPF4Qr/aRIACrZZ2Ib", - "p0ETzaMgFM24Ym+Ir4IPfhfDAHN957ZgKfrX5oUPnz7YdgnBjEelIHxW0HxWW7b3NMe+YywjODd8KgqS", - "0MUWzsIVkSslBNb1rTdvaK/3BxaIggedT98inDE11sqUjdVrroVgic9PBj0KlJpCcw2TF/eLGNf3Nc37", - "40tDbPNYxIktFoTPnOMtaOMZYCKWj6POjUKstVKBhZKejNyqE4Dm+shW1GjoRRaYHJCNLsuiYFwKbff9", - "6+rqHP3w6gpULPzjgqSUk0QemmUFWuNtFdX694UmnGM7WX0K9rNCoOIJYHChDjkwueWKUI7WbK4k5l1l", - "6Idj4J/CtoCHFqv1HGdByxrjnGQaJXSBckLSSKzNSlJ7pXOfUTXafiA54XBKnV2do0KbpxVu+yNCQc4Y", - "t53SGMPeh99vzifGYfO51BXjCVkAp7D8e5pJwkVfCPq8czAEskMfTNOgfitKXjDRE/oPbaoLH7eE0wUN", - "Y8TVAB2OtePDBxh0OukPNwSnM4M/RPcWpbfaiSKzcw0SdMZrPWZ0d1eADE6mQPDgsvIU9LlAlbmwQKXw", - "I26VFRz0072IbdRFoDn6ZSOeayS+QIyjXwTLs/S5numF8QLBzt4x7LtX92vvvs9pG82IpmErWwdLegS3", - "wT4mmOoLWoDDhiqe8OwPjuEmK3Va5MsQslc4w/kSrFKcptoDMN4cW8Q8cqXDw5erqeNp6imUdc/WVCq1", - "L7ZCkjWCywsIY5jTqMfzr6P0XbQJxZzvxqOUrXHohJrA33fYt9aI+qB8Q+SKRVBwfTG1GGgP0Yev9mhC", - "GFpQLiQi6Vfffvu3f6KinGc0gcsmtkCT6QQ9N4c2mKXa355MJy/6sBnnT8tkA1m0ujxtqf5fNoEgSnVR", - "jy7pMicp+vHdlXKzqls1tbX6Zi1+qRvxhur54R7qMnAPpZdSww/Racm5voYEPy/PtkhoU46kzoeKKZ79", - "spHP+k0SB7gxoMA5lipcDb2XOlNexrl1OkXsYAIHQSFOux0Fply4xmTltuqwRkmz1ITgGCdhpw89v/j+", - "9O//+OafL7T5rpkMBpn4hTadtQNpw8zguPjzQVgldEjq0GjYgDG/CpJwEjYXWk5x3B2951W8v8LYgbgJ", - "n13LoXSTcAOF6ZyTAnMC0XZ1TpxErKeYdWLGIx2uVzM0ohG7X4AYBXuoFOya5YdbvM6C2tZbaGImaISr", - "do1t3AA/2yQAoX2w9yPlLL0fdQchHonqofu9QVR6HIr3O9YDSB7N5PBoHr/80cL/TDTE35dzOzxIFX8l", - "XjNy1/HdlCFwbMSKpLPgdLtv4PzkohvsmNPMcS501BtNJ5AcYhxkgsoiYet22MpNp9nBw6lQNY4RK+Ds", - "DmOpHfmzw+8I8OKAXC9cppTkiQYzbDe9Vx+9H5lQn4kCp1Xsw4SHg5RLQ3SbaGLpZEZzyeH4XXXUf87K", - "PGyCPn6G2iAeC4/8nVPVPs0qz0kAg7oEDfBjzUL35b0LIspM7syBMf23l/SnmhNaHBa+zaFpMotNpo20", - "ei9VBlTgIJV8G2Cji+tXiC7cy3iTMrclEuFbTDM8z4i9KjHRjbNzm6utr8bAl6B5qhwXUqccSKYHoGZK", - "IKK5kARDakTSpgR6PiELwrmX/gURwReRsLXLd4nLRxVCXDRabHTxoGGl4ZzYHdtr5LFSkqViRyvEAbVj", - "rcERuvNSrEIm2RArshSrhhFhBnep89/BfozlYo0j4LgM0YOeoYwBBsnuRhsMG2yodWWFmmTbvFzP4XoJ", - "S8SJiRYLPzvUHAXWw7u+mLoJo1ggrDxvKuktsXmmSgH4I+pcU4GwhAlTKpQfZK6vYu8+0LyUWpPIbUET", - "nGVbnRGUYbWi8rxXjEv0nBwuD8doTuSGkBx9C5cof3/50gL6IvaoQVuBJaexJw31JsBeU9jWqQwsAHSV", - "1sOEJKlRhIAyhSdB82VGDkoBTyUIJyZhWONXFCQBLHq3OO3r6PB1a6+J6G7VeyrS4O8YYw6NPlyQJRWS", - "cDDET8HYesU543EOhy/RV4cv66t7NYXJ6CFqcMd5DL8H4uaAa3RyeTqdmjngtkxjJ3iowlfd8el/lWuc", - "H3CCUzgA9eyQmuB8Z/lZr1pF6lIyL5fL8OINWuk9OYTpReoDqBPV7d10iSp1E+YIR+UbCDQZ3+rbyubU", - "a2mb2qikOq5K8vQA4kUmB8QThq4ctKCEX1+8tiDAFfqGzFGBl8R4e2DxOhe7eM5K2edEQAQtkV02tv5Y", - "1CpX571thfYKYTwqCCsyYhmfKmxVGSx6+bGjE8ka0wzhNOVECJ0KNTyToc6R6oK6Zgc/Owq7KAFFl2Vs", - "U+VsVdfcJNWRRnEcyFkao5Lnx5TIxTHEHcUxJBgfw1IHaqnjQBLMbtv8ZfMxlJ0LcD8T+kR8R+boJ7JF", - "l0SilCXlWu0JwK7es9nMlHrTz4QTYnfT0OorOLV2Lw/aQ8HGnJMgaM9/fPfTCw/A+4BWoyljS9YLmjER", - "zKGlDjM1rLqB6JCHgmU02Q5bAKITQud8rXxNUXB6i5Mt0tPVtIFxetY5EWjFNtq6IEXGtvAF40uc15lA", - "WUYSKcaKNcUYcQIYG4O9oEySjAkiUEG4YDnOdKpQ2HXSuRlqY11SY4XBfq+TVKeVDmhgEFUpQ+B/gUgJ", - "mybSFhtHFHeTBS9iOUzqvUyxtuAnOIdULPPXSJwvoAx2F+RIzljo1a8ocEIOhPLjIGMkowLcbP1IVIMQ", - "3UrrQVb/g1W2kBvMw9fIJ6jM6a8lMU9WldNluR/MV3R9PZ28QFgIfcnlPVxFKbklmTpnEePIrqOFW6wI", - "r9JxfOPJ4B1kyizrzVpNpM/bdJvjtTlSuDEVIiGoaqu3hIugsXSCzE+BDftsX4NRfQl7ee8iNBK7189n", - "7UYhyjtbR+5VL6qHBGbdRj6qHlcBp8MSXbybs5yMkXexM1O2f/NvcyxocojespxUObJqFaOb9ccCPc/B", - "q0G4KMTY5mipf7xwHlPnTKIVviVIzy2qTMbj4KJhnIkHK2RJ+BoChcK8IalUcoO2DQ2ts3k5TmQJ0R2d", - "ISZWtKi8N8/QwyaN2J3N/wDiSEJLq1U7/hHafZXdYRM/yKzufW4FN7C1mCn2w1Xqns3EblrhPbeiIePG", - "kb/OZzHpTEcfgw8ZrpT7jqVhRNfiq4V7g0U7au0+Hf1Dugb1hXEQefpn48vru3IrwlZDs3xB6xdMFkhH", - "uVRufkOl9ELV+bYkShI9VsdN9ATq0HgJ5RLMn5UW0T91kurJbXpym57cpie36cltenKbntymJ7fpyW36", - "y7tN3rV6O73R8yI6+cy3oD70OGQ7XnRcSsbvVfxDSMZ3LXuhPguq4HtdQ8NsDjq6tzLwzjk2yQ41Qe6D", - "mY6CIH3b2y3l7LpIsSTN5P0ovTs/r25gheRloiWzVAPU7m9Oo7V86iST4Kukh79FMHnsC5qRyArm15v6", - "cOlNPDeztcaO/f0EoHd4tBv9A2l4gzOqpjmv+YGkA8X2Vo8178Jbr1uVOixofp/gSSSRLuBXBd8vogYo", - "O76AvFa62kDeR8o2QA6FepH7cCL1p7Dcl0rx6gVnBYgjied1h8z0yqBtQGEG7GJVR1IxpZOKGXorFlf/", - "FQytHN1+1A+lIeF0sa2l9XRFko+xPEn9cTAlz/EZFphmJScoUVMhkzsVekNGko+h92NqFOwznprRHgY5", - "EGhNhMBLcu/XVjfON8aWHWD5wEYsZMGFXMp1IHxwcl5zkr5Xpw7FXOj6Ulp/j/ehA99NNjHgPpyMZHt2", - "EGG3x8uxtTufVd42ZWffryof6ZniXRxrQ176dSJuyDlRaRgvF1j08bGSKj+DdhducoWyK9c2uqEdUeLm", - "7A7RwF7Jjz+NDu7Umy3pjOHkAajtU5MeWrsZbCc15cJQKSq/JkPQYKyB2ZvCbVuONUidJLmPygzhYYjS", - "dKHaWW3CT38AvRna/APwt6vu3IG376U8Y+Larz6DuxqMmXcky37K2SY/K0g+nZy6NQBDzKU+QvqrriLV", - "A582OoUhz86fCSfY7+fmv+q65nViX7Pq9XikMDIg0Y/QWwcGgKjW/wHuNa62rSA9hdqe9kpkt3uDhmOp", - "Acc5y7drVoqZKWfftwdb5Mq8CIqUt7JRSdwoWwWpEjhYQ0u/EJArVkqE66QE/QjBFsqjAi1w5j2lrJ4K", - "KWVYB/93oPtEh/2RCRRcuFcInbT3r5Eej/zevI/IAdolfTw4fzZlAj4EL5SosI8/7getH3LeRXw1z3WQ", - "rvPZrfu81gehg+IB5MZ0w+6S6JzlXRqz4puh8YPGZPql9F6UsGaEINnnWCYrt4yXS/HOcq0DvzMb7v6q", - "IRVNXkzrBhyDjtV2a42+F7VBfI0ttUIb6uTG8M7C+IuzV4AjdmQytU2aL5i+XoHcHnjts8Y0Gx2PViTL", - "2P9IXgo5z1hymJLbkW1dMrpSf/4uYwmSBK8Vi0Htv9FKykIcHx35wxSVGq+o7fCb00urg/z+EaaMH85T", - "z6oyZbnefX2Kbk4PTs6nbi1GjZlvbqCWgGQJc2uDHVnzxq2Oq8eZytKj8SijCTHGn9npSYGTFTn46vBl", - "a5ObzeYQw8+HjC+PzFhx9Hp6+urt5Ss15lB+0qaaa5lRuKZ0wj62Rvjzm9PLFzrSpe8WRi8P1cIQviE5", - "LujoePT14UuApcByBcx+ZPbn8NVR3fOgYPHLGeGivL5yUboC2zJ2o3MmZA2rqDodmBuc71i6tRxEtMQ7", - "dQqPlDtV98Pqk83uO467uzvnZIfdffXy5U6LN/y0uxZnnv0E4i/K9RrzbR+m2jI1rsix5KwsxNFn+O90", - "chegz9Fn/d/p5E4BtwylSF4QySm5JaJZLiBGrx9IkFyFU63o50iZ5h8UqOY2RrlqwGO10JudjFztKHlJ", - "xm0E16d5OxdG7zi8hKh/Hb7Ghy/OFAOI0sUajgISR6Z+dW12AJgH9rYrLL+2m0OwCm/zyriqrNJmlgEt", - "MfYh573LPoKo33N9c4IO4YL7EWEX3ih0eYQDKBNxkGKJgUt+O3BK9YQZxBRWsKZ3sNqUW3/MqaHpFeMJ", - "nAd65khxpX1wy6C6TnvmmGHFe4ZwzdBaYPfiE++aKXL0m3S8qlyIo76qhhuSVRkafhsC02nA1Iv2qy3H", - "WMWrMLNPBqnX+ULc0CxbshP9vbo7gyldilXjpOjVBS2Km+Q+t84W5MSDqYPce3UdFPLY07n0aFA7Urpj", - "X0TvqRQSZ4E+AkXLrOxCKCEZ3+1Mhywt8dATvS+VbR+k6F5zz7LYk9w2RCTvg/ldeMHk5JADP7rUww82", - "F0VEE3lKJ3PJ54IBqUj7YITeZffMC/3pO0PYYTjie5jApPuJo89VEuBd9f8mCdD3C2EgsMQAd83WFY46", - "bG7q4QNctsZyBvDuNesUx538uLAkTJvF3SNhikbF5H2dPKHC4b9LbAIAQclQQ2IYO3qaq+pDxmiaPPFl", - "xLBybtfcXiTK1poGo5puBJLm0IfQFPTyU05ErLNcqO939Sk0dc3Yxju93OYhbemxjRlqrrb9X/YlQ+FG", - "N3s+H2L9RgYJW1+nnB7p6xS6ww3JsgNot3dkWgAmzcueWHiw5LlA3qA2fc/gZ31bMNojgjuzBoZF1rTX", - "6e0nhNmeiGalg9K9qqCG7nl81fMQBqpY9KC+Z3sEJvJUhL2C+oJMFboLvQ9rtdDzAC6b/Pm5TJ3wR5Wx", - "G+WVWCaOwa598W3ODN1uF1JmqjeqzZ5+bqmGBsPRNKns+L47jN6a9ICzX0vCtzXSmmXlH0Ckq1C9jti6", - "7qu+B6x5gqokPZQS3ijDrbyV6tLTNo+Exmam5GWwzuXYvLg1I1OEl8oSkbpnZnRDLCWzOmPwgbsyz10B", - "5g2uO17qPZqmbHaxYSDVTyJ3pGmwZqp9NK8vgJQ7eICXpiiJV+PAfV1fxdpsW81si4iQWD+UTut+gsEl", - "Tc0Vr/+mkw1VcAbyxbh+b7rGH+3n0XKmYYmoywfsjiydbGSrzWqJ71lQv5nfjUFy2yBVF4vxKsVUxWHW", - "mOrWy7pHqPuG18a2odczzrI5Tj5qCzyIetO7VOjMKb2meYJvqGsw7TCCmtLnBr1A3ar08l9n168nlQVv", - "EqVvTdWVhDMhDgSVNbQLxpeEb6OIrF433Z+/bUlm5YDckq0wJSv035wqM86DAfVv02amarDO5grxh+iN", - "bSYcWcRxYDTzbxX3wPk88+8dKop59KE5SrDOww30LRYxTIWrUO+EOZ0y8kygOukqJ4m0DXmuL15rctvW", - "8zTLoHKDrcvMbgnfVkILqk0SvqY5cRD6TKGowHOaUUmJAHatCiccootXp2dv3rx6O3k1UZiociVrxF10", - "i55epU5aupcIQixsBVcINSe8Oflf2K6SvrqushU10xBW0jX9jVSC8wzacxMO3QoeYXfw4nOlkwN3SlBw", - "mjXbyhQ61TIhHBSKIZut6UE+SVtcpOHsE36ITqLNkdVxXBdKKrAwjYpxHuyGX6kBe8DXIYca86aIVqv5", - "vds3GvqqqiFmBtNEWYPp6a32bq7qddelkEjijxDOYErbs9LWQag6M5uuBssSKyOQaAAYp0uaq5/NXqgp", - "asbHKLHNGHGOsJRKMUfo6wL/oHyRr19+1eGrfDrYbDYHC8bXByXPSK7MitR3XsKlCmIN0NrHjC6YU/Vc", - "NSdZ6CiKjga71xQig3rt2RbhBRAezD7T9EIdi1TSpQ0TcSo+Ku2ZEfwxUjgm/CjYbsf2lH+vP3w/clhu", - "g6smytbidKqbBPpZq72RTzgxha12aS3TfANln2b3xVS/Z2WeNhxFCPD03cnXlTAq52nI7TucB8I7QGlu", - "e9hrJYHzBn6q1t5t72jvV+vuFfcXCdEFHv0Nce4bQbluQhWYxylU1STMU5s9E+5AoE2/bGsLEbbMRnVc", - "L4kUzc4Odb0qpSpdIwiLdtsC26PAOUd53R8/3uyrzSzB3gO7XQfurAyj/Uf+coZovCZoGB3JsMKivt99", - "/MeIEPSAGS2DdQ/Pv7Mg0F/XsqsMsD+yVddZu3CAkvj/FY35gk04dw7cDDULnyIz4cJDq+AzuT+YE90C", - "3Y8PHP/pYyB97Zs6Ggv7x2zIs2gbxX971FzHWNeogHV8ahq/341H37z8NvCSXR+yb5lEJ7oKLHz6t6+j", - "hSnRq1xSuUVXjKHXmC8JDPjqnwFlwhh6g/OtxbsIGeqRPmsDfCzjT7rmeyvnWH0Q67C1JzOXprrDYsDh", - "m5g3r6CxdDu3vFneExzdQmu9SqVVteprc/fmXE+2i0q+lNWRHPZjoJCRaZSrbw7bNZ6K2PYsRDXYLIdS", - "6GvG4cLSPndzqxGISF2HfpEK5O9elkp9KCi/Df38va7Y0nzVZQwmUc7XVEaaAqsPHOuYs3K5Qjenl00O", - "vS1cDrUnT/wGVUmA/Qqwv8J5muk622ZlJ3On3d9ZHY1MnUUlQaw0Dzaqm9tILr5yAC8saD1XqU75yfpZ", - "iJP4Grtte9i1nw3kdd1tPCTIF9RuBiEBHeUgq0MfVWLRGe5x6zMD/XSZG/AOsHL5OREr87MtUlvFhNgi", - "FPDTsQNtUq2wMJ6ucsYg6idKWHJRZh0dr9scArK8PzXZ4fLagOLYRhTrsv3K1XAVpn3uGw2SKr4pM+ig", - "axkl6JEOcTEA2e1A5IPWnVWVVkL+Ot8Wki05Lla23jXOU7b2yh87Pl/d4zzeLNNrjeGY9b3Q1mUSBvsf", - "7VrwEW9kUHlPjy3sCFBxQ8Dv9idbLPfeG9CKZZsjLu0Jjpi60JTbohsWRTrkkOgSdb2wxwuWxnFiy3sC", - "uLwq2J+bftB11/qe1RuWscMFH4Yf049i+Z6AGgO11BcPVsZuwAD4Dqeojl231LxXBKdb13cm4Nmi9E9J", - "0q0zViNGeDX8cd5qu1Qr9ZvTy6iCDVk1egEduN/TPUhHB6s9+35dpdr7fL+X+4TCb48bAKVH8uyUhhEq", - "8oUl0B6Z/vuE5kvIunZb2DuECmpPvuGTb9jnG863tevnPp3wH3jouJdXAhCO4bCz6NTXi3P0Z/kJioVk", - "mK4dF9JnY1t/YuqMhPfkD03DDpQcA0jckmNuuYvS1he6R2GVPjQvidSLO86NCbsbt9t9R3MYRnRf4ZMJ", - "xLzrF8/hc1HRZPeM6orAu78D1AU6+22JiQ3ZV1g0s+7XqLhprGb76ezVrGi/92uWCd7Xg79gWet9P2WN", - "lUAe9IK1WRR7gBba/+vAvy6zVu/OaJo4OvtLvK27Of8S3NpYcidm/eLn7TBOd1d5BIX8u7D476GOXWNu", - "r/q4VTX7i2jkYFXlHXRy4aMnxKtqGPi7msPqooPHR0cZS3C2YkIe/9fLf7wcKYKYKZo8ocP2Bzo2mOom", - "U43r02Yu7ajNWRaugfNU2wiE9/WN/YrgTK6QLVJvxum/6j/efbj7vwAAAP//O03XmU67AAA=", + "H4sIAAAAAAAC/+x9/XPbNrbov4LRezNJZiQ724/dt36/XNdKu2qT2OuvzJ0m44FISEJCESwAWlEz/t/v", + "4AAgARIgKdtKu7f+qY1FAAcH5/scHHwZJWxdsJzkUoyOvoxEsiJrDP97nCREiEv2ieTnRBQsF0T9OSUi", + "4bSQlOWjo9EblpIMLRhH+nME3yM74GA0HhWcFYRLSmBWDJ/dSPVZe7rLFUH6CwRfICpESVI03yKpfirl", + "inH6O1afI0H4LeFqCbktyOhoJCSn+XJ0Nx4lNznLkwC8F/AJSlguMc3V/2IEnyLJ0JygUpBU/W/CCZYE", + "YVRwxhaILVDBhCBCqIXZAn0iW7TGknCKM7RZkRxx8ltJhNRTJpykJJcUZ13g3ZDPBeVE3NAAKma5JEvC", + "UUpyBrMqBGR0QSRdE0TV9hOWp0JBo34yczrrUT2DWrBrocvued3jCE/OyYITseo6U/OJnmWMNiuarFCC", + "cxflbK6OBOVk460pghgUCSsCx3t6djk7fXv8eozoAlE4ggRnana1FRhkD6qmqiSjJJf/HzG5InxDBRmj", + "81f/vpqdv5oG1wawbvSfQ5tVv1jsuVQcmAyw91tJOUlHR7/6zOEt9GE8klRmamyIL6uJ2fwjSeRoPPo8", + "kXgp1KSMpsl3CR19uBuPTiq6nFJRZHirduAzaMYSnMHOWhvP8Tr0w10NW3v+CGQKMMAKb8B1rk+nS9Kc", + "zqYnqB5hD7QtaxaMr3Fgqh/h7xXj1DPNiWK06GnB/GyhJvy/nCxGR6P/c1iLz0MjOw9/fnd5Bt/d6RlE", + "G4JjzvEWAFC/D4CESrIWwUMxf8BqxhZB6eU/hA7IInp30hmiDdpn1KUQComVOIwIkGP088XpWyQColuz", + "lyjnQu0ml9m2KU6wA8UBenN1calkTsGJILnUkttBOxUoZxJxIkueR2ggqluiUO5BwZw8XMEAuPQxtUyN", + "SLUay8npYnT0a5tmvzRI7k7RV4xZXax6UC48LjaGQideGsxhVvTgjrDKvcXshcSyDAgAhzUEfNJmDFEN", + "jfD8l579mQnM58GdXXifBPcVlNJ63GkROK9T+B8BUkCNBW7wTsXf5rC99G1BgTJwF68+JyucL8mxa0ye", + "sJQMUD1EjwUeLOUKJSwlaMHZWtMfR0z9ubVHVtyowxiwz+pLZ6+9AD984zGBbn9B64eiQH6+oemAc4bP", + "hm1+AFM6u5/lVFIsidJM353MBhy2HdFSZjMhSqWw0HnM8vAclZuUSEyzkBQohWRr+jsRaLPCEn2ieaoE", + "mrF/ZxqhG5xLZQyjJb0FNXJ9chGW+hmm65sUSxw6SY1k2NkZJxOLUCU01RH+mLHNgZpab/eC8FuaKPNf", + "CoQFOj2DkRucZUQiXBQZTWB3belRQULytGA0DyD5RP2O7O9Whpv9AjFtVoR7FhFMidTm0AoLo01rWx4v", + "JOFIlIC5RZllW4QTtWUg1F5/QvsAN9Qc+Q01R3xT8qwN/tX5a1fvAC2YoUp9u/vC6B2g7ABd4k9EKMsj", + "UXtKCGK3hBvn42ZDsuxTzjaVmkcF5nhNJOEHaLZAc6ZYrQNIhPO0PRnmBAyagrNbmirLQ1sShqvtTPUu", + "1M42NMusAYMSINHIlzSvtHBBcppO7GcT+9nR4WEXvitIh3jqmvYOVyxLCXdJUFOsnhLVm09YvqDLkutv", + "rs5fhyGpSOzGA6BDY7s/dM9o7a+QYTNV6GwYoAKJFSuzVNF2wnJBYacC6XnSUW0mjVKFZmWl9YBgnbbo", + "buCD7jkkWRcZUFwacHnNjwEvRjOpMc02K5oRn0MTlidZmWqLjgqwRjlO1MQHlQ8OvryauOBsoaagojpa", + "bUuXSkGVmaRF5i9vIAuz/JLjXEbceCOJEpxb1rGMAKOM2yZXnJXLlYbd4ddL9e/6Q0degfWvEeHq0dwP", + "eilB64e6QMnSHKndcCQkKQSIhTZvp2SBy0yq9XwlpKYI4sE1ToIseIuzkhiHpQqaNNShIlOluwr8W0ls", + "vEVLPiSVaqOicn3mSstBPKacT4zjBsDqcA1s2ErBDZWryHpqhyAeyGeJBJGoLFBaAsQFJ7eUlcLBVB3o", + "QUoC01siEDZbU/j2z3CMqNTOIgUKJerfNLdQW6CPfaCNOWC3H0CRgB8sxuv1NCDGP317elnRCs2RZ/lo", + "Xb3I2EaLjoKTCa40+Y2mE2H92+B5W+kfIf0TLXBFrSWAhs0hwjbI54Ios0AZC4b9NE0XhCv5pI4ARLJP", + "xDZOg6aaRoEpmnHF3hBfBR/8LoYB5vrObcZS51+bFz58WrHtEoIZj0pB+E1B85vasr2nOfYDYxnBuaFT", + "UZCELragC1dErhQTWNe33rw5e70/sEAUPOhs9hbhjKmxlqdsrF5TLQRLfHoy6FGg1Cc01zCpjWqNXBkk", + "aWWRtDdsd7LI8FIJ+lQxDdi9eiPKts2R5DgXWgEg0AdmYiV1jB0VAMQJQEas/Pv6CP2BriFOQiz0xRYL", + "wm8cPRs0Ng0wERPM0StGMtfiscBCsXFGbpUqorm2HRRuGwKaBSaHU0cXZVEwLoU2QP91eXmGfnp1CbIe", + "/nFOUspJIg/MsgKt8bYKr/37XFOQY8RZwQ6GvEKgIk7gNKG0Ldj+ckUoR2s2V6z7rvI4wsH4z2GjxEOL", + "Fb+O16KZnnFOMo0SukA5IWkk6GdZur3Smc8xGm0/kZxwINzTyzNUaDu5wm1/aCpIGeO2dxwj2PvQ+/XZ", + "1HiOPpW68mRKFkApLP+RZpJw0RcLP+scDBH10AezNChoi5IXTPTkIEKb6sLHLeF0QcMYcSVAh4fvBBMC", + "BDqb9sc9gtOZwR+ie4uet9qJOmYnHxOMCtRyzCiRrkgdqMhAFOOiclm0gqLKblmgUvihv8ocDwYMvNBx", + "1FehOfq4Ec81El8gxtFHwfIsfa5nemHcUTD4d4w/79UP3LsTdtJGM6Jp2NzXUZsexm2Qj4nq+owWoLCh", + "gic8+4ODyclKaYt8GUL2Cmc4X4J5jNNUuyLGrWSLWGhAyfBwljd1XF49hXIz2JpKJfbFVkiyRpBFgXiK", + "0UY9IYg6XdB1NqHg9914lLI1DmmoKfx9h31riagV5RsiVyyCgqvzmcVAe4hWvtq1CmFoQbmQiKTffP/9", + "3/6JinKe0QSyXmyBprMpem6UNtjH2vGfzqYv+rAZp09LZANJtMritkT/x00gmlNVDKALusxJin5+d6n8", + "vSq9p7ZWp/ji2eWIW1bPDwmxi0BCTC+lhh+gk5JznQ8FhzPPtkhoU46kzoeKKJ593Mhn/SaJA9wYUOCo", + "pQpXQxNkp8rdObPer4gpJvBUFOK0/1NgyoVrTFb+s46vlDRLTSyQcRL2PtHz8x9P/v6P7/75Qpvvmshg", + "kAmkaNNZe7I23g0elD8fxHdCSlLHaMMGjPlVkISTsLnQ8s7jfvE9awL8FcYOxE347FrOSTcPbiAznXFS", + "YE4g7K/0xHHEeopZJ2Y80nkDNUMjLLJ7JsYI2AMlYNcsP9jidRaUtt5CUzNBI262a5DlGujZViMI7YO9", + "Hyln6f2oOxrySKceSjQOOqXHOfF+x3rAkUdLSrwzj2ehNPM/Ew329/ncDg+eir8Srwm5S303eQgcG7Ei", + "6U1wut03cHZ83g12zGl2oi+zKVSpGAeZoLJI2LodP3PrelrLtMJDyjnuw472962jk/6YsY2Lqy6/qTqA", + "cYwEAi70MELdkeo7vJkAhQ8oZcNlSkmeaDDD1th79dH7kYlkmiB3WkVUTPQ7eFBpiBqmmgR0rabJ4Tje", + "XJ3UmLMyDxu2j1+AN4hywyP/4Eq8zzeVPyaAQN0DDdBjTUL3pb1zIspM7kyBMam6l+qumhJaFBZOVtE0", + "uYlNpk2/ei9VgVdAPUu+DZDR+dUrRBdurYGpCNwSifAtphmeZ8RmgkzM5PTMlqLrzB94KDbCXVdUSKYH", + "oGbFI6K5kARD5UfSPgn0fEoWhHOvug3ijC8iwXCX7hKXjiqEuGi02OiiQUNKwymxO2LYKNOlJEvFjraN", + "A2rHWoPjfmelWIUMvSG2aSlWDdPEDO4S53+AVRorNRtHwHEJogc9QwkDzJzdTUEYNtj86yp6NbXEebme", + "Q/YMS8SJiUELv/jVqALrN16dz9x6WCwQVv48lfSW2DJaJQD8EXUprUBYwoQpFcq7Mtm52LUWNC+lliRy", + "W9AEZ9lWFzxlWK2o/PkV4xI9JwfLgzGaE7khJEffQ2rm7y9fWkBfxO5saNuy5DR2Y6PeBFiBCtu6UoMF", + "gK6qlpiQJDWCEFCm8CRovszIpBRwE4RwYuqhNX5FQRLAopcbamfbw9nk3sCFu1XvJkyDvmOEOTSmcU6W", + "VEjCwbw/AWPrFeeMxykcvkTfHLysKxPUFKZgiajBHfoYfg9E4wHX6PjiZDYzc0AOTmMnqFThq+6o97/K", + "Nc4nnOAUFKCeHSovnO8sPetVq/hfSublchlevHFWek/OwfQi9QGnE5Xt3ecSFeomeBKO9TcQaAra1beV", + "zanX0ja1EUl1tJbk6QSiUKbExWOGrhK7IIdfnb+2IECFwIbMUYGXxPiQYPE66WI8Z6XscyIgLpfILhtb", + "fyxqkavL+rZC+5owHhWEFRmxhE8VtqoCHb382JGJZI1phnCaciKErvQaXqhRl4B1QV2Tg1/8hV2UgKDL", + "MrapStKq5DlJdfxSHAVKssao5PkRJXJxBNFMcQT100ew1EQtdRSo8dltmx83n0LFxwD3M6E14jsyR7+Q", + "LbogEqUsKddqTwB2dV3PFt7Um34mnMC9W2VXJ/bU2r00aJWCjWQnQdCe//zulxcegPcBrUZTxpasFzRj", + "IhilpZSZGlblNTr4oWAZTbbDFoDohNAlbStfUhSc3uJki/R09dnAOD3rnAi0YhttXZAiY1v4gvElzutC", + "pywjiRRjRZpijDgBjI3BXlAmScYEEaggXLAcZ7oSKuw66YoPtbEurrHMYL/XNbizSgY0MIiqiijwv4Cl", + "hC0+abONw4q78YIXBx3G9V4hXJvxE5xDpZn5ayR6GBAGuzNypCQudKlZFDghE6H8OKhDyagAN1vfgdUg", + "RLfSum/Wfx+XLeQG83By+hiVOf2tJOZGrnK6LPWD+YqurmbTFwgLoVNn3r1clJJbkik9ixhHdh3N3GJF", + "eFXk4xtPBu/AU2ZZb9ZqIq1v022O10alcGMqREJQ1VZvCRdBY+kYmZ8CG/bJvgaj+hL28t5FaCQjoG8H", + "241C7PhmHcnWnlf3JMy6jXJbPa4CToclumg3ZzkZIy9ddKNs/+bf5ljQ5AC9ZTmpSoDVKkY2648Fep6D", + "V4NwUYixrfxS/3jh3BXPmUQrfEuQnltUhZpHwUXDOBMPFsiS8DUECoW5IlOJ5MbZNiS0LlbmOJElRHd0", + "3ZlY0aLy3jxDD5sqaXc2/wOIIwnNrVbs+Cq0O0HeYRM/yKzuvU0Ged2azRT54aog0BaaN63wnlxryLhx", + "+K/z1o8uXCVp8J7GpXLfsTSE6Fp8NXNvsGhHrd2bsX9K16BOQweRp382vrzOwFsWthKa5QtaX9CyQDrC", + "pXLzGyKlF6rOqzPRI9FjddxET6CUxkvoBmH+rKSI/qnzqJ7cpie36cltenKbntymJ7fpyW16cpue3Ka/", + "vNvkpdXbRZOeF9FJZ74F9aHHIdsx0XEhGb9XbxMhGd+1q4f6LCiC75WGhtkcdHRvZWDOOTbJDi1P7oOZ", + "jn4nfdvbreTsqkixJM0rAdHz7vy8ysAKyctEc2apBqjdX59EWxXVRSbBu04Pv+FgquMXNCORFcyv17Vy", + "6S1nN7O1xo79/QSgd2i0G/0Dz/AaZ1RNc1bTA0kHsu2tHmuuvbcu7ypxWND8PsGTSCFdwK8K3opEDVB2", + "vFd5pWS1gbzvKNsAOSfUi9yHH1J/Cct9TynenOG0AHYk8WrxkJleGbQNKMyAXazqSCmmd4U77b/pWov/", + "CoZWjW4/6oeeIeF0sa259WRFkk+xOkn9cbAkz/EZFphmJScoUVMhUzsVuplGkk+hW2lqFOwzXprRHgY1", + "EGhNhMBLcu87XNfON8aWHWD5wEYsZMGF3JPrQPjg4rzmJH13WZ0Tc6HrK2n9I26dDryN2cSAex0zUu3Z", + "cQi7XYmOrd15WfO2yTv7vqv5SJcf7+JYG3J/sBNxQ/REJWG8WmDRR8eKq/wK2l2oyWXKrlrb6IZ2RIlb", + "sztEAnsdTf5jZHCn3GxxZwwnD0Btn5j00NpNYDuJKReGSlD5nR6CBmMNzN4EbttyrEHqPJL7iMwQHoYI", + "TReqncUm/PQnkJuhzT8Af7vKzh1o+17CM8au/eIzuKvBmOm+Hde+ILjwWipBmrrdUwlNkF7Xb/3Y7glk", + "HOWJ6fORVOmOQKOQV86txY54gO0e2O3WfxWn34M6AqMjJLqPYqBD8o5k2S852+SnBcln0xO3ZWVIWKiP", + "kP6qq6f6wAuwTh/T07Nnwkne+HctXnWl7Z1Y5k3VYyDSxxuYws+4WIcUgKjW/wnyVJfbVtKFQitam+La", + "LQ/UCBRowHHO8u2aleLGvL7Qtwfbyczc8Ip0Y7NRZtzosgalLzjY8k3f+JArVkqE6yITfanE9nWkAi1w", + "5l24dRqyucmcHc59qtM4yAR+zt2UUOfZ+2nBxzt+b95HpAAdYng8OH81zSQ+BBOEVNjLPPeD1k8h7MK+", + "muY6j651ORsyFIuMbR6JA2y71iodbXpp1AoI2lhS3b73u+uT2XBK77wF7t729jHYQbAB2oiJtoG4213e", + "uMqlQy9U3HFPJaO7BuxF1WhyDxL3HMtk5ba0I8MMhMHfmQ13f9Xg/SbHpfWrOIOMwfZ7N333wIP4GtvT", + "Cm2ok2jDOwvjL05eAYrYkcjUNmm+YDopCBVpcEdtjWk2OhqtSJax/5K8FHKeseQgJbcj+57Q6FL9+YeM", + "JUgSvFYkBn0wRyspC3F0eOgPU6fUuPtvh1+fXFhp4z/qYsxXnKeeL2Ba1L379gRdn0yOz2Zug1SNme+u", + "oa+GZAlz++QdWqPcbVmtx9VtSjOaEOOymJ0eFzhZkck3By9bm9xsNgcYfj5gfHloxorD17OTV28vXqkx", + "B/KzdjBcf4JCct0JVtrG/c+vTy5e6PisNo5HLw/UwhB0JDku6Oho9O3BS4ClwHIFxH5o9ufQ1WH9EEnB", + "4ilF4aK8ThQqWYFtS8fRGROyhlVUz4+YvOMPLN1aCiKa452enYcfhZZUmvv6eLM7M3d3d+fYL7C7b16+", + "3GnxRnThrkWZp78A+4tyvcZ824epNk+Nq+NYclYW4vAL/Hc2vQucz+EX/d/Z9E4BtwwV9p4TySm5JaLZ", + "5CJ2Xj+R4HEVjsv5a6R3+k8KVJNDpOrvisZqpjc7GbnSUfKSjNsIrp25dgWX3nF4CVH/OnyND1+dKAYc", + "ShdpOAJIHJqm8rXZAWBObI42zL/2iZVga+xmoUPVZahNLAPeqdkHn/cu+wisfs/1jQYdQgX3O4RdaKPQ", + "TT0mEOeYpFhioJLfJ07bqjCBmHYg1sEIdl5ze/E5/WS9xlQBfaBnjjQa2we1DOpxtmeKGdZyagjVDO2L", + "dy868ZKjEdVvikirJjeO+KpewZGsqivy3wYxz3+Y3ul+5/EYqXh9kfZJIPU6X4kams12djp/r1vU4JMu", + "xaqhKXplQevETUmq23MObnKAqeNHnyH05ZGnk6prnHak4cy+Dr2nv02cBPoOKNocaJeDEpLx3XQ61BaK", + "h2r0vgLMfRxF95p75sWekswhLHkfzO9CC6aSjEz86FIPPdgKKhEtPyudejufCgYU0O2DEHqX3TMt9Bed", + "DSGH4YjvIQITThWHX6os1l31/yaL5fuFMBBIYoC7ZntsRx02N3f2AJetsZwBvHvNOke3kx8X5oRZ86GD", + "SJii0T18X5on1ET/D4lNACAoGWpIDCNHT3JVjwMymiZPdBkxrJwMivsuj7K1ZsGophuBpDk8Dmra0PmF", + "UiL23GPoMf7qU3hpOWMbT3u5D+m0ucemxGuqtm8h7YuHwo8+7Vk/xN7eGcRsfa9G7Zn7JvBTbly7JzZ0", + "ADCIqZ0Ze1Ey8FjWIKbchUHs6k+M8kiM0skfBxuSZRN4LPbQPGCbNLOisTh6yXOBvEHtcz6Fn3VabbRH", + "BHcWEQ0LQevwjLefEGZ7Qv+VlEj3KiQa0uHxhcNDCKgi0UmdkH4EIvJEhc3VfkWiChUN3Ie0Wuh5AJVN", + "//OpTCnjw8orjNJKrDDPYNc29DA6Qz8WDxV0VQuC5ou0bieeBsHRNKkc3r5kX+9DJoCz30rCtzXSmm+R", + "POCQLkPtmGLrupe2H7DmMapqsFFKeOOVBeXWV9UB9uljeA3TdDQOtjEem4YKZmSK8FKZ7FK/+BzdEEvJ", + "TV0Q/sBdmW4GAPMG1+816z2alzztYsNAqm+873imwZbYtieKzpSWgvAJXpqeU14LG7d5SmXH2Uehsy0i", + "QmLdByOtX8MNLmlaanmvRzvFkQVnwF+M63YCa/zJfh7tVh3miLo7zO7I0rWHtpm45vieBXVLlN0IJLfP", + "e+teYF4jsKr31xpTXT2uX7h2WzTYJBDOU5TgLJvj5JN2VYOoNy9vC11Iqdc0HVbM6RpMO4SgpvSpQS9Q", + "P7R98a/Tq9fTytU192BuTVOthDMhJoLKGtoF40vCt1FEVpdX70/ftuO+8tRvyVaYjkT6b04TMec+mPq3", + "qafcYNNyg80V4g/QG/sUfmQRx9PXxL9V1AP6+cZP0FUn5p0PzVGC9TWLwKv7Ioap8CMDO2FOu2DPBKqr", + "E3OSSPuK29X5a33c5t9wm6EUpGq7z24J31ZMC6JNEr6mOXEQ+kyhqMBzmlFJiQByrfriHKDzVyenb968", + "ejt9NVWYqEqna8Sdd7OeLR615s+9WBCCxivItdWU8Ob4v2G7ivvqtvmW1cxz5pKu6e+kYpxnAh4O5/AY", + "zSPsDi70r3Sx7U6VPCBnTZdB03hIV14nhINAMcdmWzaRz9L2jmo44IQfoOPo0/5KHdd98AoszDP7OHe9", + "d/A63YcNKgVfx+ZqzJseibxZClH17rGPcashZgZ97d2A6cmt9m4u63XXpZBI4k8QYmBK2rPStrmxd+nt", + "ozXLEisjkGgAGKdLmqufzV6o6VnJxyixL/jiHGEplWCOnK8L/IMKq759+U2Hr/J5stlsJgvG15OSZyRX", + "ZkXqOy/hTjSxVzPbakb3Q6se6jaaLKSKoqPB7jV9JuE5jmyL8AIOHsw+86aRUotU0qWNp3IqPinpmRH8", + "KdIXLNzzwW4HUa3r3+sP348cktvAo6PEtTid5lVtiwT2Rj7jxPQt3OXlsOYVV9t5oy/58CMr87ThKEKA", + "p694pW50VDlPQ8pUQB8IT4HSHNliORASOG/gB64OhL2jvdeguLUgXyVEF7jTPcS5bwTlug+qwDx+QlXL", + "2Ty1ZWbhB2a06ZdtbZ/Zltmo1PWSSNF8uKduR6hEpWsEYdF+lcY+QePoUTtfa+FuVzr4tMxuefOdhWH0", + "eam/nCEab/kcRkcyrG+073cf/TkiBD1gRrsc3sPz7+z39te17CoD7M9s1XW2ph0gJP53RWO+4svNOwdu", + "hpqFT5GZcF+5VfDW7J/MiY5ezK2yR//hMZC+1/k6XqP31WzIs2gbxX971KLg2KOAAev4RL9boY7wu5ff", + "BxqVaCX7lkl0rJt8w6d/+zbadxi9yiWVW3TJGHqN+ZLAgG/+GRAmjKE3ON9avIuQoR55RnOAj2X8Sdd8", + "bxXnqw9iDyjuycylqX5AN+DwTc0VeJBY+rXOvNm9GRzdQku9SqRVT5HU5u71mZ5sF5F8ISuVHPZjoE+d", + "eV1dZw7bLfyK2PYsRDXYLIeXLtaMQ8LS3gt1m82ISNuefpYKFLpflEp8KCi/D/38o27I1bz+aAwmUc7X", + "VEZeklcfONYxZ+Vyha5PLpoUelu4FGo1TzyDqjjAfgXYX+E8zfQzCmZlp8St9aY+qEamdFFJECvNzaYq", + "cxu5tKIcwHMLWk8q1ekuXN+fcirEY9m2h6X9bCCvK7fxkCBfULoZhARklIOsDnlUsUVnuMdtvw/np7uY", + "gXeAlcvPiViZn20P8iomxBahgJ+OHWiTaoWF8XSVMwZRP1HCkosyixB3mEKAl/cnJjtcXhtQHNuIYv0q", + "i3I1XIFp78VHg6SKbsoMHki3hBL0SIe4GIDsdiDyQeveVI20Qv463xaSLTkuVvY5A5ynbO11t3d8Piu6", + "ScdbyN7LR45Z3wtt3XZksP/Rfuoj4o0M6t7skYUdASJuCPjd/mSL5N57A1qxbKPi0p7giGn7T7ntwWNR", + "pEMOie5A2gt7vB91HCe2ezOAy6v3WHLz3D+vLKye1RuWsUMFH4ar6UexfI9BjIFY6osHK2M3YAD8gFNU", + "x65bYt7ridUt6zsL8OybI09lzC0dqxEjvCdacN56Va8W6tcnF1EBG7Jq9AI6cL+nPEjHA4V79v26XuLo", + "8/1e7hMK//XzACg9nGenNIRQHV+YA63K9K8SNK8M1605w94hNMh88g2ffMM+33C+rV0/946Rf+lCx728", + "Dq+ghsPOotM+NU7RX+Rn6KqTYbp2XEifjG2jlpkzEhovPLQMO9BiDyBxOxC6fWFK24jrHh2I+tC8JFIv", + "7jg3Juxu3O7m3ZZgn9puZTyFmHfdGiCsF9WZ7F5RXR3w7hdmdf/lfltiakP2FRbNrPs1Kq4bq9nn0vZq", + "VrQvxja7wO/rZmzw1YJ93/mOdbgfdNW7+ebBACm0/2u0f11ire6d0TRxZPbXuFt3ffY1qLWx5E7E+tX1", + "7TBKd1d5BIH8h5D4HyGOXWNur/K49SjCV5HIwab5O8jkwkdPiFbVMPB3NYXV3TmPDg8zluBsxYQ8+n8v", + "//FypA7ETNGkCR22n+jYYKrfEGykT5u1tKM2ZVm4Bs5TbSMQ3tcZ+xXBmVwh+waJGaf/qv949+HufwIA", + "AP//L1a6WgzCAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index a38197089..b64e15514 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -10,6 +10,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "log" @@ -40,6 +41,12 @@ const ( jwtProofTypHeader = "openid4vci-proof+jwt" ) +type walletInitiatedFlowParams struct { + credentialIssuer string // IssuerURL + claimsEndpoint string + credentialTemplateID string +} + type OIDC4CIConfig struct { InitiateIssuanceURL string ClientID string @@ -229,6 +236,186 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { return nil } +func extractWalletInitiatedFlowParams(scopes []string) *walletInitiatedFlowParams { + for _, scope := range scopes { + chunks := strings.Split(scope, "||") + if len(chunks) != 3 { + continue + } + + return &walletInitiatedFlowParams{ + credentialIssuer: chunks[0], + claimsEndpoint: chunks[1], + credentialTemplateID: chunks[2], + } + } + + return nil +} + +func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error { + log.Println("Starting OIDC4VCI authorized code flow Wallet initiated") + + // Check whether scope contains combined string Issuer URL||ClaimEndpoint||credentialTemplateID + walletInitiatedFlowData := extractWalletInitiatedFlowParams(config.Scope) + if walletInitiatedFlowData == nil { + return errors.New( + "undefined scopes supplied. " + + "Make sure one of the provided scope is a combined string ||||") + } + + oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig( + walletInitiatedFlowData.credentialIssuer, + ) + if err != nil { + return fmt.Errorf("get issuer oidc issuer config: %w", err) + } + + oidcConfig, err := s.getIssuerOIDCConfig(walletInitiatedFlowData.credentialIssuer) + if err != nil { + return fmt.Errorf("get issuer oidc config: %w", err) + } + + redirectURL, err := url.Parse(config.RedirectURI) + if err != nil { + return fmt.Errorf("parse redirect url: %w", err) + } + + var listener net.Listener + + if config.Login == "" { // bind listener for callback server to support log in with a browser + listener, err = net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("listen: %w", err) + } + + redirectURL.Host = fmt.Sprintf( + "%s:%d", + redirectURL.Hostname(), + listener.Addr().(*net.TCPAddr).Port, + ) + } + + s.oauthClient = &oauth2.Config{ + ClientID: config.ClientID, + RedirectURL: redirectURL.String(), + Scopes: config.Scope, + Endpoint: oauth2.Endpoint{ + AuthURL: oidcConfig.AuthorizationEndpoint, + TokenURL: oidcConfig.TokenEndpoint, + AuthStyle: oauth2.AuthStyleInHeader, + }, + } + + issuerState := uuid.New().String() + state := uuid.New().String() + + b, err := json.Marshal(&common.AuthorizationDetails{ + Type: "openid_credential", + Types: []string{ + "VerifiableCredential", + config.CredentialType, + }, + Format: lo.ToPtr(config.CredentialFormat), + }) + if err != nil { + return fmt.Errorf("marshal authorization details: %w", err) + } + + authCodeURL := s.oauthClient.AuthCodeURL(state, + oauth2.SetAuthURLParam("issuer_state", issuerState), + oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"), + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.SetAuthURLParam("authorization_details", string(b)), + ) + + var authCode string + + if config.Login == "" { // interactive mode: login with a browser + authCode, err = s.getAuthCodeFromBrowser(listener, authCodeURL) + if err != nil { + return fmt.Errorf("get auth code from browser: %w", err) + } + } else { + authCode, err = s.getAuthCode(config, authCodeURL) + if err != nil { + return fmt.Errorf("get auth code: %w", err) + } + } + + if authCode == "" { + return fmt.Errorf("auth code is empty") + } + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, s.httpClient) + + var beforeTokenRequestHooks []OauthClientOpt + + if hooks != nil { + beforeTokenRequestHooks = hooks.BeforeTokenRequest + } + + for _, f := range beforeTokenRequestHooks { + f(s.oauthClient) + } + + s.print("Exchanging authorization code for access token") + token, err := s.oauthClient.Exchange(ctx, authCode, + oauth2.SetAuthURLParam("code_verifier", "xalsLDydJtHwIQZukUyj6boam5vMUaJRWv-BnGCAzcZi3ZTs"), + ) + if err != nil { + return fmt.Errorf("exchange code for token: %w", err) + } + + s.token = token + + err = s.CreateWallet() + if err != nil { + return fmt.Errorf("create wallet: %w", err) + } + + s.print("Getting credential") + vc, _, err := s.getCredential( + oidcIssuerCredentialConfig.CredentialEndpoint, + config.CredentialType, + config.CredentialFormat, + walletInitiatedFlowData.credentialIssuer, + ) + if err != nil { + return fmt.Errorf("get credential: %w", err) + } + + b, err = json.Marshal(vc) + if err != nil { + return fmt.Errorf("marshal vc: %w", err) + } + + s.print("Adding credential to wallet") + if err = s.wallet.Add(s.vcProviderConf.WalletParams.Token, wallet.Credential, b); err != nil { + return fmt.Errorf("add credential: %w", err) + } + + vcParsed, err := verifiable.ParseCredential(b, + verifiable.WithDisabledProofCheck(), + verifiable.WithJSONLDDocumentLoader( + s.ariesServices.JSONLDDocumentLoader())) + if err != nil { + return fmt.Errorf("parse vc: %w", err) + } + + log.Printf( + "Credential with ID [%s] and type [%v] added successfully", + vcParsed.ID, + config.CredentialType, + ) + + if !s.keepWalletOpen { + s.wallet.Close() + } + + return nil +} + func (s *Service) getIssuerOIDCConfig( issuerURL string, ) (*issuerv1.WellKnownOpenIDConfiguration, error) { diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 4d5394730..43aadb967 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -190,6 +190,38 @@ paths: $ref: '#/components/schemas/InitiateOIDC4CIRequest' tags: - issuer + '/issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal': + parameters: + - schema: + type: string + name: profileID + in: path + required: true + description: Issuer Profile ID. + - schema: + type: string + name: profileVersion + in: path + required: true + description: Issuer Profile Version. + post: + summary: Initiate OIDC Credential Issuance + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/InitiateOIDC4CIResponse' + operationId: initiate-credential-issuance-internal + description: Internal endpoint used by Wallet to initiate OIDCI credential issuance interaction. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/InitiateOIDC4CIRequest' + tags: + - issuer /issuer/interactions/push-authorization-request: post: summary: Push Authorization Details @@ -800,12 +832,16 @@ components: pre-authorized_grant_anonymous_access_supported: type: boolean description: JSON Boolean indicating whether the issuer accepts a Token Request with a Pre-Authorized Code but without a client id. The default is false. + wallet_initiated_auth_flow_supported: + type: boolean + description: JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. required: - authorization_endpoint - token_endpoint - response_types_supported - scopes_supported - grant_types_supported + - wallet_initiated_auth_flow_supported - pre-authorized_grant_anonymous_access_supported WellKnownOpenIDIssuerConfiguration: title: WellKnownOpenIDIssuerConfiguration response @@ -1224,6 +1260,8 @@ components: tx_id: type: string description: Transaction ID to correlate upcoming authorization response. + wallet_initiated_flow: + $ref: '#/components/schemas/WalletInitiatedFlowParameters' required: - authorization_request - authorization_endpoint @@ -1250,6 +1288,26 @@ components: - client_id - client_secret - scope + WalletInitiatedFlowParameters: + title: WalletInitiatedFlowParameters + x-tags: + - issuer + type: object + description: If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data. + properties: + profileID: + type: string + profileVersion: + type: string + claimEndpoint: + type: string + credentialTemplateID: + type: string + required: + - profileID + - profileVersion + - claimEndpoint + - credentialTemplateID InitiateOIDC4CIRequest: title: InitiateOIDC4CIRequest type: object @@ -1300,6 +1358,9 @@ components: claim_data: type: object description: Required for Pre-Authorized Code Flow. VCS OIDC Service acts as OP for wallet applications + wallet_initiated_issuance: + type: boolean + description: Boolean flags indicates whether given transaction is initiated by Wallet. x-tags: - issuer InitiateOIDC4CIResponse: diff --git a/pkg/profile/api.go b/pkg/profile/api.go index 71d91850c..c1be98bf6 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -80,6 +80,7 @@ type OIDCConfig struct { EnableDynamicClientRegistration bool `json:"enable_dynamic_client_registration"` InitialAccessTokenLifespan time.Duration `json:"initial_access_token_lifespan"` PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported"` + WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` } // VCConfig describes how to sign verifiable credentials. diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index 1bfc498e6..48085009c 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -397,6 +397,34 @@ func (c *Controller) InitiateCredentialIssuance(e echo.Context, profileID, profi return util.WriteOutput(e)(resp, nil) } +// InitiateCredentialIssuanceInternal initiates OIDC credential issuance flow. +// Only for internal usage. Should bot be exposed to the Internet. +// POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal. +func (c *Controller) InitiateCredentialIssuanceInternal(e echo.Context, profileID, profileVersion string) error { + ctx, span := c.tracer.Start(e.Request().Context(), "InitiateCredentialIssuanceInternal") + defer span.End() + + profile, err := c.accessProfile(profileID, profileVersion) + if err != nil { + return err + } + + var body InitiateOIDC4CIRequest + + if err = util.ReadBody(e, &body); err != nil { + return err + } + + span.SetAttributes(attributeutil.JSON("initiate_issuance_request", body, attributeutil.WithRedacted("claim_data"))) + + resp, err := c.initiateIssuance(ctx, &body, profile) + if err != nil { + return err + } + + return util.WriteOutput(e)(resp, nil) +} + func (c *Controller) initiateIssuance( ctx context.Context, req *InitiateOIDC4CIRequest, @@ -416,6 +444,7 @@ func (c *Controller) initiateIssuance( CredentialExpiresAt: req.CredentialExpiresAt, CredentialName: lo.FromPtr(req.CredentialName), CredentialDescription: lo.FromPtr(req.CredentialDescription), + WalletInitiatedIssuance: lo.FromPtr(req.WalletInitiatedIssuance), } resp, err := c.oidc4ciService.InitiateIssuance(ctx, issuanceReq, profile) @@ -502,16 +531,27 @@ func (c *Controller) prepareClaimDataAuthorizationRequest( return nil, resterr.NewSystemError("OIDC4CIService", "PrepareClaimDataAuthorizationRequest", err) } + var walletInitiatedFlowParams *WalletInitiatedFlowParameters + if resp.WalletInitiatedFlow != nil { + walletInitiatedFlowParams = &WalletInitiatedFlowParameters{ + ProfileID: resp.WalletInitiatedFlow.ProfileID, + ProfileVersion: resp.WalletInitiatedFlow.ProfileVersion, + ClaimEndpoint: resp.WalletInitiatedFlow.ClaimEndpoint, + CredentialTemplateID: resp.WalletInitiatedFlow.CredentialTemplateID, + } + } + return &PrepareClaimDataAuthorizationResponse{ + WalletInitiatedFlow: walletInitiatedFlowParams, AuthorizationRequest: OAuthParameters{ ClientId: profile.OIDCConfig.ClientID, ClientSecret: profile.OIDCConfig.ClientSecretHandle, - ResponseType: resp.ResponseType, Scope: resp.Scope, + ResponseType: resp.ResponseType, // empty if resp.WalletInitiatedFlow }, AuthorizationEndpoint: resp.AuthorizationEndpoint, - PushedAuthorizationRequestEndpoint: lo.ToPtr(resp.PushedAuthorizationRequestEndpoint), - TxId: string(resp.TxID), + PushedAuthorizationRequestEndpoint: lo.ToPtr(resp.PushedAuthorizationRequestEndpoint), // empty if resp.WalletInitiatedFlow + TxId: string(resp.TxID), // empty if resp.WalletInitiatedFlow }, nil } @@ -791,6 +831,7 @@ func (c *Controller) getOpenIDConfig(profileID, profileVersion string) (*WellKno config.GrantTypesSupported = profile.OIDCConfig.GrantTypesSupported config.ScopesSupported = profile.OIDCConfig.ScopesSupported config.PreAuthorizedGrantAnonymousAccessSupported = profile.OIDCConfig.PreAuthorizedGrantAnonymousAccessSupported + config.WalletInitiatedAuthFlowSupported = profile.OIDCConfig.WalletInitiatedAuthFlowSupported if profile.OIDCConfig.EnableDynamicClientRegistration { var regURL string diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index b1abb0605..4217c3ca6 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -90,6 +90,9 @@ type InitiateOIDC4CIRequest struct { // Required for Pre-Authorized Code Flow. Boolean value specifying whether the issuer expects presentation of a user PIN along with the Token Request in a pre-authorized code flow. UserPinRequired *bool `json:"user_pin_required,omitempty"` + + // Boolean flags indicates whether given transaction is initiated by Wallet. + WalletInitiatedIssuance *bool `json:"wallet_initiated_issuance,omitempty"` } // Model for Initiate OIDC Credential Issuance Response. @@ -175,6 +178,9 @@ type PrepareClaimDataAuthorizationResponse struct { // Transaction ID to correlate upcoming authorization response. TxId string `json:"tx_id"` + + // If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data. + WalletInitiatedFlow *WalletInitiatedFlowParameters `json:"wallet_initiated_flow,omitempty"` } // Model for Prepare Credential request. @@ -261,6 +267,14 @@ type ValidatePreAuthorizedCodeResponse struct { TxId string `json:"tx_id"` } +// If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data. +type WalletInitiatedFlowParameters struct { + ClaimEndpoint string `json:"claimEndpoint"` + CredentialTemplateID string `json:"credentialTemplateID"` + ProfileID string `json:"profileID"` + ProfileVersion string `json:"profileVersion"` +} + // OpenID Config response. type WellKnownOpenIDConfiguration struct { // URL of the OP's OAuth 2.0 Authorization Endpoint. @@ -283,6 +297,9 @@ type WellKnownOpenIDConfiguration struct { // URL of the OP's OAuth 2.0 Token Endpoint. TokenEndpoint string `json:"token_endpoint"` + + // JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. + WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` } // OpenID Config response. @@ -322,6 +339,9 @@ type PostIssueCredentialsJSONBody = IssueCredentialData // InitiateCredentialIssuanceJSONBody defines parameters for InitiateCredentialIssuance. type InitiateCredentialIssuanceJSONBody = InitiateOIDC4CIRequest +// InitiateCredentialIssuanceInternalJSONBody defines parameters for InitiateCredentialIssuanceInternal. +type InitiateCredentialIssuanceInternalJSONBody = InitiateOIDC4CIRequest + // PostCredentialsStatusJSONRequestBody defines body for PostCredentialsStatus for application/json ContentType. type PostCredentialsStatusJSONRequestBody = PostCredentialsStatusJSONBody @@ -349,6 +369,9 @@ type PostIssueCredentialsJSONRequestBody = PostIssueCredentialsJSONBody // InitiateCredentialIssuanceJSONRequestBody defines body for InitiateCredentialIssuance for application/json ContentType. type InitiateCredentialIssuanceJSONRequestBody = InitiateCredentialIssuanceJSONBody +// InitiateCredentialIssuanceInternalJSONRequestBody defines body for InitiateCredentialIssuanceInternal for application/json ContentType. +type InitiateCredentialIssuanceInternalJSONRequestBody = InitiateCredentialIssuanceInternalJSONBody + // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -470,6 +493,11 @@ type ClientInterface interface { InitiateCredentialIssuance(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // InitiateCredentialIssuanceInternal request with any body + InitiateCredentialIssuanceInternalWithBody(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + InitiateCredentialIssuanceInternal(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // OpenidConfig request OpenidConfig(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -705,6 +733,30 @@ func (c *Client) InitiateCredentialIssuance(ctx context.Context, profileID strin return c.Client.Do(req) } +func (c *Client) InitiateCredentialIssuanceInternalWithBody(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewInitiateCredentialIssuanceInternalRequestWithBody(c.Server, profileID, profileVersion, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) InitiateCredentialIssuanceInternal(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewInitiateCredentialIssuanceInternalRequest(c.Server, profileID, profileVersion, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) OpenidConfig(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewOpenidConfigRequest(c.Server, profileID, profileVersion) if err != nil { @@ -1158,6 +1210,60 @@ func NewInitiateCredentialIssuanceRequestWithBody(server string, profileID strin return req, nil } +// NewInitiateCredentialIssuanceInternalRequest calls the generic InitiateCredentialIssuanceInternal builder with application/json body +func NewInitiateCredentialIssuanceInternalRequest(server string, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewInitiateCredentialIssuanceInternalRequestWithBody(server, profileID, profileVersion, "application/json", bodyReader) +} + +// NewInitiateCredentialIssuanceInternalRequestWithBody generates requests for InitiateCredentialIssuanceInternal with any type of body +func NewInitiateCredentialIssuanceInternalRequestWithBody(server string, profileID string, profileVersion string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "profileID", runtime.ParamLocationPath, profileID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "profileVersion", runtime.ParamLocationPath, profileVersion) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/issuer/profiles/%s/%s/interactions/initiate-oidc-internal", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewOpenidConfigRequest generates requests for OpenidConfig func NewOpenidConfigRequest(server string, profileID string, profileVersion string) (*http.Request, error) { var err error @@ -1331,6 +1437,11 @@ type ClientWithResponsesInterface interface { InitiateCredentialIssuanceWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceResponse, error) + // InitiateCredentialIssuanceInternal request with any body + InitiateCredentialIssuanceInternalWithBodyWithResponse(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) + + InitiateCredentialIssuanceInternalWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) + // OpenidConfig request OpenidConfigWithResponse(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*OpenidConfigResponse, error) @@ -1557,6 +1668,28 @@ func (r InitiateCredentialIssuanceResponse) StatusCode() int { return 0 } +type InitiateCredentialIssuanceInternalResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *InitiateOIDC4CIResponse +} + +// Status returns HTTPResponse.Status +func (r InitiateCredentialIssuanceInternalResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r InitiateCredentialIssuanceInternalResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type OpenidConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -1763,6 +1896,23 @@ func (c *ClientWithResponses) InitiateCredentialIssuanceWithResponse(ctx context return ParseInitiateCredentialIssuanceResponse(rsp) } +// InitiateCredentialIssuanceInternalWithBodyWithResponse request with arbitrary body returning *InitiateCredentialIssuanceInternalResponse +func (c *ClientWithResponses) InitiateCredentialIssuanceInternalWithBodyWithResponse(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) { + rsp, err := c.InitiateCredentialIssuanceInternalWithBody(ctx, profileID, profileVersion, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseInitiateCredentialIssuanceInternalResponse(rsp) +} + +func (c *ClientWithResponses) InitiateCredentialIssuanceInternalWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) { + rsp, err := c.InitiateCredentialIssuanceInternal(ctx, profileID, profileVersion, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseInitiateCredentialIssuanceInternalResponse(rsp) +} + // OpenidConfigWithResponse request returning *OpenidConfigResponse func (c *ClientWithResponses) OpenidConfigWithResponse(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*OpenidConfigResponse, error) { rsp, err := c.OpenidConfig(ctx, profileID, profileVersion, reqEditors...) @@ -2031,6 +2181,32 @@ func ParseInitiateCredentialIssuanceResponse(rsp *http.Response) (*InitiateCrede return response, nil } +// ParseInitiateCredentialIssuanceInternalResponse parses an HTTP response from a InitiateCredentialIssuanceInternalWithResponse call +func ParseInitiateCredentialIssuanceInternalResponse(rsp *http.Response) (*InitiateCredentialIssuanceInternalResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &InitiateCredentialIssuanceInternalResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest InitiateOIDC4CIResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseOpenidConfigResponse parses an HTTP response from a OpenidConfigWithResponse call func ParseOpenidConfigResponse(rsp *http.Response) (*OpenidConfigResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) @@ -2115,6 +2291,9 @@ type ServerInterface interface { // Initiate OIDC Credential Issuance // (POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc) InitiateCredentialIssuance(ctx echo.Context, profileID string, profileVersion string) error + // Initiate OIDC Credential Issuance + // (POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal) + InitiateCredentialIssuanceInternal(ctx echo.Context, profileID string, profileVersion string) error // Request openid-config // (GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration) OpenidConfig(ctx echo.Context, profileID string, profileVersion string) error @@ -2263,6 +2442,30 @@ func (w *ServerInterfaceWrapper) InitiateCredentialIssuance(ctx echo.Context) er return err } +// InitiateCredentialIssuanceInternal converts echo context to params. +func (w *ServerInterfaceWrapper) InitiateCredentialIssuanceInternal(ctx echo.Context) error { + var err error + // ------------- Path parameter "profileID" ------------- + var profileID string + + err = runtime.BindStyledParameterWithLocation("simple", false, "profileID", runtime.ParamLocationPath, ctx.Param("profileID"), &profileID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter profileID: %s", err)) + } + + // ------------- Path parameter "profileVersion" ------------- + var profileVersion string + + err = runtime.BindStyledParameterWithLocation("simple", false, "profileVersion", runtime.ParamLocationPath, ctx.Param("profileVersion"), &profileVersion) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter profileVersion: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.InitiateCredentialIssuanceInternal(ctx, profileID, profileVersion) + return err +} + // OpenidConfig converts echo context to params. func (w *ServerInterfaceWrapper) OpenidConfig(ctx echo.Context) error { var err error @@ -2349,6 +2552,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/issuer/interactions/validate-pre-authorized-code", wrapper.ValidatePreAuthorizedCodeRequest) router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/credentials/issue", wrapper.PostIssueCredentials) router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/interactions/initiate-oidc", wrapper.InitiateCredentialIssuance) + router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/interactions/initiate-oidc-internal", wrapper.InitiateCredentialIssuanceInternal) router.GET(baseURL+"/issuer/:profileID/:profileVersion/.well-known/openid-configuration", wrapper.OpenidConfig) router.GET(baseURL+"/issuer/:profileID/:profileVersion/.well-known/openid-credential-issuer", wrapper.OpenidCredentialIssuerConfig) diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 887a4ff00..18eb2ea46 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -262,17 +262,6 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e return fmt.Errorf("decode claim data authorization response: %w", err) } - oauthConfig := oauth2.Config{ - ClientID: claimDataAuth.AuthorizationRequest.ClientId, - ClientSecret: claimDataAuth.AuthorizationRequest.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: claimDataAuth.AuthorizationEndpoint, - AuthStyle: oauth2.AuthStyleAutoDetect, - }, - RedirectURL: c.issuerVCSPublicHost + "/oidc/redirect", - Scopes: claimDataAuth.AuthorizationRequest.Scope, - } - if params.State != nil { ar.(*fosite.AuthorizeRequest).State = *params.State } @@ -282,18 +271,42 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e return resterr.NewFositeError(resterr.FositeAuthorizeError, e, c.oauth2Provider, err).WithAuthorizeRequester(ar) } + var walletInitiatedFlowData *oidc4ci.WalletInitiatedFlow + + if claimDataAuth.WalletInitiatedFlow != nil { + walletInitiatedFlowData = &oidc4ci.WalletInitiatedFlow{ + ProfileID: claimDataAuth.WalletInitiatedFlow.ProfileID, + ProfileVersion: claimDataAuth.WalletInitiatedFlow.ProfileVersion, + Scope: claimDataAuth.AuthorizationRequest.Scope, + ClaimEndpoint: claimDataAuth.WalletInitiatedFlow.ClaimEndpoint, + CredentialTemplateID: claimDataAuth.WalletInitiatedFlow.CredentialTemplateID, + } + } + if err = c.stateStore.SaveAuthorizeState( ctx, params.IssuerState, &oidc4ci.AuthorizeState{ - RedirectURI: ar.GetRedirectURI(), - RespondMode: string(ar.GetResponseMode()), - Header: resp.GetHeader(), - Parameters: resp.GetParameters(), + RedirectURI: ar.GetRedirectURI(), + RespondMode: string(ar.GetResponseMode()), + Header: resp.GetHeader(), + Parameters: resp.GetParameters(), + WalletInitiatedFlow: walletInitiatedFlowData, }); err != nil { return fmt.Errorf("save authorize state: %w", err) } + oauthConfig := oauth2.Config{ + ClientID: claimDataAuth.AuthorizationRequest.ClientId, + ClientSecret: claimDataAuth.AuthorizationRequest.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: claimDataAuth.AuthorizationEndpoint, + AuthStyle: oauth2.AuthStyleAutoDetect, + }, + RedirectURL: c.issuerVCSPublicHost + "/oidc/redirect", + Scopes: claimDataAuth.AuthorizationRequest.Scope, + } + var authCodeURL string if len(lo.FromPtr(claimDataAuth.PushedAuthorizationRequestEndpoint)) > 0 { @@ -383,6 +396,16 @@ func (c *Controller) OidcRedirect(e echo.Context, params OidcRedirectParams) err return apiUtil.WriteOutput(e)(nil, err) } + if resp.WalletInitiatedFlow != nil { + + // todo what auth token I need to use? + //req.Header.Add("Authorization", "Bearer "+token) + if err = c.walletInitiateIssuance(ctx, params.State, resp.WalletInitiatedFlow); err != nil { + return fmt.Errorf("walletInitiateIssuance: %w", err) + } + //todo: to I need to store in cache authorization details and then call c.issuerInteractionClient.PushAuthorizationDetails()? + } + storeResp, storeErr := c.issuerInteractionClient.StoreAuthorizationCodeRequest(ctx, issuer.StoreAuthorizationCodeRequestJSONRequestBody{ Code: params.Code, @@ -414,6 +437,43 @@ func (c *Controller) OidcRedirect(e echo.Context, params OidcRedirectParams) err return nil } +func (c *Controller) walletInitiateIssuance(ctx context.Context, issuerState string, walletInitiatedFlow *oidc4ci.WalletInitiatedFlow) error { + // if wallet initiated flow - need to create transaction here + initiateIssuanceResponse, err := c.issuerInteractionClient.InitiateCredentialIssuanceInternal(ctx, + walletInitiatedFlow.ProfileID, + walletInitiatedFlow.ProfileVersion, + issuer.InitiateCredentialIssuanceJSONRequestBody{ + WalletInitiatedIssuance: lo.ToPtr(true), + ClaimEndpoint: lo.ToPtr(walletInitiatedFlow.ClaimEndpoint), + CredentialTemplateId: lo.ToPtr(walletInitiatedFlow.CredentialTemplateID), + GrantType: lo.ToPtr("authorization_code"), + OpState: lo.ToPtr(issuerState), + ResponseType: lo.ToPtr("code"), + //Scope: lo.ToPtr([]string{"openid", "profile"}), + Scope: lo.ToPtr(walletInitiatedFlow.Scope), + UserPinRequired: lo.ToPtr(false), + + AuthorizationDetails: nil, + ClaimData: nil, // pre-auth flow only + ClientInitiateIssuanceUrl: nil, + ClientWellknown: nil, + CredentialDescription: nil, + CredentialExpiresAt: nil, + CredentialName: nil, + }) + + if err != nil { + return err + } + + defer initiateIssuanceResponse.Body.Close() + + if initiateIssuanceResponse.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code %d", initiateIssuanceResponse.StatusCode) + } + + return nil +} // OidcToken handles OIDC token request (POST /oidc/token). func (c *Controller) OidcToken(e echo.Context) error { @@ -453,6 +513,7 @@ func (c *Controller) OidcToken(e echo.Context) error { txID = resp.TxId } else { + //todo make wallet init specific exchangeResp, errExchange := c.issuerInteractionClient.ExchangeAuthorizationCodeRequest( ctx, issuer.ExchangeAuthorizationCodeRequestJSONRequestBody{ diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index 9646dd63b..ff696ce68 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -81,6 +81,7 @@ type TransactionData struct { CredentialExpiresAt *time.Time CredentialName string CredentialDescription string + WalletInitiatedIssuance bool } // AuthorizationDetails are the VC-related details for VC issuance. @@ -117,6 +118,7 @@ type InitiateIssuanceRequest struct { CredentialExpiresAt *time.Time CredentialName string CredentialDescription string + WalletInitiatedIssuance bool } // InitiateIssuanceResponse is the response from the Issuer to the Wallet with initiate issuance URL. @@ -135,6 +137,7 @@ type PrepareClaimDataAuthorizationRequest struct { } type PrepareClaimDataAuthorizationResponse struct { + WalletInitiatedFlow *WalletInitiatedFlow ProfileID profileapi.ID ProfileVersion profileapi.Version TxID TxID @@ -167,18 +170,28 @@ type InsertOptions struct { } type AuthorizeState struct { - RedirectURI *url.URL `json:"redirect_uri"` - RespondMode string `json:"respond_mode"` - Header map[string][]string `json:"header"` - Parameters map[string][]string `json:"parameters"` + RedirectURI *url.URL `json:"redirect_uri"` + RespondMode string `json:"respond_mode"` + Header map[string][]string `json:"header"` + Parameters map[string][]string `json:"parameters"` + WalletInitiatedFlow *WalletInitiatedFlow `json:"walletInitiatedFlow,omitempty"` +} + +type WalletInitiatedFlow struct { + ProfileID string `json:"profileId"` + ProfileVersion string `json:"profileVersion"` + Scope []string `json:"scope"` + ClaimEndpoint string `json:"claimEndpoint"` + CredentialTemplateID string `json:"credentialTemplateId"` } type eventPayload struct { - WebHook string `json:"webHook,omitempty"` - ProfileID string `json:"profileID,omitempty"` - ProfileVersion string `json:"profileVersion,omitempty"` - OrgID string `json:"orgID,omitempty"` - Error string `json:"error,omitempty"` + WebHook string `json:"webHook,omitempty"` + ProfileID string `json:"profileID,omitempty"` + ProfileVersion string `json:"profileVersion,omitempty"` + OrgID string `json:"orgID,omitempty"` + WalletInitiatedFlow bool `json:"walletInitiatedFlow,omitempty"` + Error string `json:"error,omitempty"` } type AuthorizationCodeGrant struct { @@ -206,6 +219,12 @@ type CredentialOfferResponse struct { Grants CredentialOfferGrant `json:"grants"` } +// wellKnownOpenIDConfiguration OpenID Config response. +type wellKnownOpenIDConfiguration struct { + // JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. + WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` +} + type ServiceInterface interface { InitiateIssuance(ctx context.Context, req *InitiateIssuanceRequest, profile *profileapi.Issuer) (*InitiateIssuanceResponse, error) //nolint:lll PushAuthorizationDetails(ctx context.Context, opState string, ad *AuthorizationDetails) error diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 4e92496d7..eb28c9c2b 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -169,12 +169,20 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( ) (*PrepareClaimDataAuthorizationResponse, error) { tx, err := s.store.FindByOpState(ctx, req.OpState) if err != nil { + if errors.Is(err, ErrDataNotFound) { + // check if issuer supports Wallet initiated flow + if walletInitiatedFlowScopeData := s.isWalletInitiatedFlow(req.Scope); walletInitiatedFlowScopeData != nil { + // process wallet initiated flow + return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, walletInitiatedFlowScopeData) + } + // otherwise - return regular error + } return nil, fmt.Errorf("find tx by op state: %w", err) } newState := TransactionStateAwaitingIssuerOIDCAuthorization if err = s.validateStateTransition(tx.State, newState); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } tx.State = newState @@ -207,22 +215,23 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( if req.AuthorizationDetails != nil { if err = s.updateAuthorizationDetails(ctx, req.AuthorizationDetails, tx); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } } if err = s.store.Update(ctx, tx); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } - if err = s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationRequestPrepared); err != nil { - s.sendFailedEvent(ctx, tx, err) + if err = s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationRequestPrepared); err != nil { + s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } return &PrepareClaimDataAuthorizationResponse{ + WalletInitiatedFlow: nil, ProfileID: tx.ProfileID, ProfileVersion: tx.ProfileVersion, TxID: tx.ID, @@ -233,6 +242,125 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( }, nil } +func (s *Service) getIssuerOIDCConfig( + issuerURL string, +) (*wellKnownOpenIDConfiguration, error) { + // GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration + resp, err := s.httpClient.Get(issuerURL + "/.well-known/openid-configuration") + if err != nil { + return nil, fmt.Errorf("get issuer well-known: %w", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("get issuer well-known: status code %d", resp.StatusCode) + } + + var oidcConfig wellKnownOpenIDConfiguration + + if err = json.NewDecoder(resp.Body).Decode(&oidcConfig); err != nil { + return nil, fmt.Errorf("decode issuer well-known: %w", err) + } + + return &oidcConfig, nil +} + +type walletInitiatedFlowScopeParsed struct { + scope string + issuerURL string + claimsEndpoint string + credentialTemplateID string +} + +func (s *Service) isWalletInitiatedFlow(requestScopes []string) *walletInitiatedFlowScopeParsed { + for _, scope := range requestScopes { + //if parsedURL, err := url.Parse(scope); err != nil || parsedURL.Host == "" { + // continue + //} + chunks := strings.Split(scope, "||") + if len(chunks) != 3 { + continue + } + + //todo: make a call to well-known. + //issuerOidcConfiguration, err := s.getIssuerOIDCConfig(chunks[0]) + //if err != nil { + // logger.Error("getIssuerOIDCConfig", log.WithError(err)) + // return nil + //} + // + //fmt.Println(issuerOidcConfiguration) + //if issuerOidcConfiguration.WalletInitiatedAuthFlowSupported { + if true { + return &walletInitiatedFlowScopeParsed{ + scope: scope, + issuerURL: chunks[0], + claimsEndpoint: chunks[1], + credentialTemplateID: chunks[2], + } + } + } + + return nil +} + +func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( + ctx context.Context, + requestScopes []string, + walletInitiatedFlowScopeData *walletInitiatedFlowScopeParsed) (*PrepareClaimDataAuthorizationResponse, error) { + // todo: temp solution until will find a better way to pass profile ID/Version + chunks := strings.Split(walletInitiatedFlowScopeData.issuerURL, "/") + profileID, profileVersion := chunks[len(chunks)-2], chunks[len(chunks)-1] + + profile, err := s.profileService.GetProfile(profileID, profileVersion) + if err != nil { + return nil, fmt.Errorf("wallet initiated flow get profile: %w", err) + } + + oidcConfig, err := s.wellKnownService.GetOIDCConfiguration(ctx, profile.OIDCConfig.IssuerWellKnownURL) + if err != nil { + return nil, fmt.Errorf("wallet initiated flow get oidc config: %w", err) + } + + // todo: what scopes should I use? In code below I'm using scopes from incoming request but removing issuerURL + scopes := make([]string, 0, len(requestScopes)-1) + + for _, scope := range requestScopes { + if scope == walletInitiatedFlowScopeData.scope { + continue + } + scopes = append(scopes, scope) + } + + event := eventPayload{ + WebHook: profile.WebHook, + ProfileID: profileID, + ProfileVersion: profileVersion, + OrgID: profile.OrganizationID, + WalletInitiatedFlow: true, + } + + if err = s.sendEvent(ctx, spi.IssuerOIDCInteractionAuthorizationRequestPrepared, "", event); err != nil { + event.Error = err.Error() + s.sendFailedEvent(ctx, "", event) + return nil, err + } + + return &PrepareClaimDataAuthorizationResponse{ + WalletInitiatedFlow: &WalletInitiatedFlow{ + ProfileID: profileID, + ProfileVersion: profileVersion, + ClaimEndpoint: walletInitiatedFlowScopeData.claimsEndpoint, + CredentialTemplateID: walletInitiatedFlowScopeData.credentialTemplateID, + }, + ProfileID: profileID, + ProfileVersion: profileVersion, + Scope: scopes, + AuthorizationEndpoint: oidcConfig.AuthorizationEndpoint, + }, nil +} + func (s *Service) updateAuthorizationDetails(ctx context.Context, ad *AuthorizationDetails, tx *Transaction) error { if tx.CredentialTemplate == nil { return ErrCredentialTemplateNotConfigured @@ -314,7 +442,7 @@ func (s *Service) ValidatePreAuthorizedCodeRequest( //nolint:gocognit,nolintlint return nil, err } - if errSendEvent := s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionQRScanned); errSendEvent != nil { + if errSendEvent := s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionQRScanned); errSendEvent != nil { return nil, errSendEvent } @@ -331,7 +459,7 @@ func (s *Service) PrepareCredential( } if tx.CredentialTemplate == nil { - s.sendFailedEvent(ctx, tx, ErrCredentialTemplateNotConfigured) + s.sendFailedTransactionEvent(ctx, tx, ErrCredentialTemplateNotConfigured) return nil, resterr.NewCustomError(resterr.OIDCCredentialTypeNotSupported, ErrCredentialTemplateNotConfigured) } @@ -384,12 +512,12 @@ func (s *Service) PrepareCredential( tx.State = TransactionStateCredentialsIssued if err = s.store.Update(ctx, tx); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } - if errSendEvent := s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionSucceeded); errSendEvent != nil { - s.sendFailedEvent(ctx, tx, errSendEvent) + if errSendEvent := s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionSucceeded); errSendEvent != nil { + s.sendFailedTransactionEvent(ctx, tx, errSendEvent) return nil, errSendEvent } @@ -453,38 +581,27 @@ func (s *Service) requestClaims(ctx context.Context, tx *Transaction) (map[strin } func (s *Service) createEvent( - tx *Transaction, eventType spi.EventType, - e error, + transactionID TxID, + ep eventPayload, ) (*spi.Event, error) { - ep := eventPayload{ - WebHook: tx.WebHookURL, - ProfileID: tx.ProfileID, - ProfileVersion: tx.ProfileVersion, - OrgID: tx.OrgID, - } - - if e != nil { - ep.Error = e.Error() - } - payload, err := json.Marshal(ep) if err != nil { return nil, err } event := spi.NewEventWithPayload(uuid.NewString(), "source://vcs/issuer", eventType, payload) - event.TransactionID = string(tx.ID) + event.TransactionID = string(transactionID) return event, nil } -func (s *Service) sendEvent(ctx context.Context, tx *Transaction, eventType spi.EventType) error { - return s.sendEventWithError(ctx, tx, eventType, nil) -} - -func (s *Service) sendEventWithError(ctx context.Context, tx *Transaction, eventType spi.EventType, e error) error { - event, err := s.createEvent(tx, eventType, e) +func (s *Service) sendEvent( + ctx context.Context, + eventType spi.EventType, + transactionID TxID, + ep eventPayload) error { + event, err := s.createEvent(eventType, transactionID, ep) if err != nil { return err } @@ -492,7 +609,38 @@ func (s *Service) sendEventWithError(ctx context.Context, tx *Transaction, event return s.eventSvc.Publish(ctx, s.eventTopic, event) } -func (s *Service) sendFailedEvent(ctx context.Context, tx *Transaction, err error) { - e := s.sendEventWithError(ctx, tx, spi.IssuerOIDCInteractionFailed, err) +func (s *Service) sendFailedEvent( + ctx context.Context, + transactionID TxID, + ep eventPayload) { + e := s.sendEvent(ctx, spi.IssuerOIDCInteractionFailed, transactionID, ep) logger.Debugc(ctx, "sending Failed OIDC issuer event error, ignoring..", log.WithError(e)) } + +func (s *Service) sendTransactionEvent( + ctx context.Context, + tx *Transaction, + eventType spi.EventType, +) error { + return s.sendEvent(ctx, eventType, tx.ID, eventPayload{ + WebHook: tx.WebHookURL, + ProfileID: tx.ProfileID, + ProfileVersion: tx.ProfileVersion, + OrgID: tx.OrgID, + WalletInitiatedFlow: tx.WalletInitiatedIssuance, + }) +} + +func (s *Service) sendFailedTransactionEvent( + ctx context.Context, + tx *Transaction, + e error, +) { + s.sendFailedEvent(ctx, tx.ID, eventPayload{ + WebHook: tx.WebHookURL, + ProfileID: tx.ProfileID, + ProfileVersion: tx.ProfileVersion, + OrgID: tx.OrgID, + Error: e.Error(), + }) +} diff --git a/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go b/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go index bbae0e662..d7fc42041 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_exchange_code.go @@ -23,14 +23,14 @@ func (s *Service) ExchangeAuthorizationCode(ctx context.Context, opState string) newState := TransactionStateIssuerOIDCAuthorizationDone if err = s.validateStateTransition(tx.State, newState); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } tx.State = newState profile, err := s.profileService.GetProfile(tx.ProfileID, tx.ProfileVersion) if err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } @@ -50,18 +50,18 @@ func (s *Service) ExchangeAuthorizationCode(ctx context.Context, opState string) resp, err := oauth2Client.Exchange(ctx, tx.IssuerAuthCode) if err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } tx.IssuerToken = resp.AccessToken if err = s.store.Update(ctx, tx); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } - if err = s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationCodeExchanged); err != nil { + if err = s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationCodeExchanged); err != nil { return "", err } diff --git a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go index cc6b34d23..a94806cd7 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go +++ b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go @@ -53,21 +53,26 @@ func (s *Service) InitiateIssuance( // nolint:funlen,gocyclo,gocognit } data := &TransactionData{ - ProfileID: profile.ID, - ProfileVersion: profile.Version, - OrgID: profile.OrganizationID, - CredentialTemplate: template, - CredentialFormat: profile.VCConfig.Format, - OIDCCredentialFormat: s.SelectProperOIDCFormat(profile.VCConfig.Format, template), - ClaimEndpoint: req.ClaimEndpoint, - ResponseType: req.ResponseType, - OpState: req.OpState, - State: TransactionStateIssuanceInitiated, - WebHookURL: profile.WebHook, - DID: profile.SigningDID.DID, - CredentialExpiresAt: lo.ToPtr(s.GetCredentialsExpirationTime(req, template)), - CredentialName: req.CredentialName, - CredentialDescription: req.CredentialDescription, + ProfileID: profile.ID, + ProfileVersion: profile.Version, + OrgID: profile.OrganizationID, + CredentialTemplate: template, + CredentialFormat: profile.VCConfig.Format, + OIDCCredentialFormat: s.SelectProperOIDCFormat(profile.VCConfig.Format, template), + ClaimEndpoint: req.ClaimEndpoint, + ResponseType: req.ResponseType, + OpState: req.OpState, + State: TransactionStateIssuanceInitiated, + WebHookURL: profile.WebHook, + DID: profile.SigningDID.DID, + CredentialExpiresAt: lo.ToPtr(s.GetCredentialsExpirationTime(req, template)), + CredentialName: req.CredentialName, + CredentialDescription: req.CredentialDescription, + WalletInitiatedIssuance: req.WalletInitiatedIssuance, + } + + if req.WalletInitiatedIssuance { + data.State = TransactionStateAwaitingIssuerOIDCAuthorization } if err = s.extendTransactionWithOIDCConfig(ctx, profile, data); err != nil { @@ -123,7 +128,7 @@ func (s *Service) InitiateIssuance( // nolint:funlen,gocyclo,gocognit return nil, fmt.Errorf("store tx: %w", err) } - if errSendEvent := s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionInitiated); errSendEvent != nil { + if errSendEvent := s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionInitiated); errSendEvent != nil { return nil, errSendEvent } diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index e4feb18e7..83af8e0c9 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -27,12 +27,12 @@ func (s *Service) StoreAuthorizationCode( tx.IssuerAuthCode = code if err = s.store.Update(ctx, tx); err != nil { - s.sendFailedEvent(ctx, tx, err) + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } - if err = s.sendEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationCodeStored); err != nil { - s.sendFailedEvent(ctx, tx, err) + if err = s.sendTransactionEvent(ctx, tx, spi.IssuerOIDCInteractionAuthorizationCodeStored); err != nil { + s.sendFailedTransactionEvent(ctx, tx, err) return "", err } diff --git a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store.go b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store.go index b38c143a2..07285b3e3 100644 --- a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store.go +++ b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store.go @@ -60,6 +60,7 @@ type mongoDocument struct { PreAuthCodeExpiresAt *time.Time CredentialName string CredentialDescription string + WalletInitiatedIssuance bool } // Store stores oidc transactions in mongo. @@ -229,6 +230,7 @@ func (s *Store) mapTransactionDataToMongoDocument(data *oidc4ci.TransactionData) PreAuthCodeExpiresAt: data.PreAuthCodeExpiresAt, CredentialDescription: data.CredentialDescription, CredentialName: data.CredentialName, + WalletInitiatedIssuance: data.WalletInitiatedIssuance, } } @@ -266,6 +268,7 @@ func mapDocumentToTransaction(doc *mongoDocument) *oidc4ci.Transaction { PreAuthCodeExpiresAt: doc.PreAuthCodeExpiresAt, CredentialDescription: doc.CredentialDescription, CredentialName: doc.CredentialName, + WalletInitiatedIssuance: doc.WalletInitiatedIssuance, }, } } diff --git a/test/bdd/bddtests_test.go b/test/bdd/bddtests_test.go index b6cc5e175..b2c3c0534 100644 --- a/test/bdd/bddtests_test.go +++ b/test/bdd/bddtests_test.go @@ -39,7 +39,7 @@ var logger = log.New("vcs-bdd") func TestMain(m *testing.M) { // default is to run all tests with tag @all but excluding those marked with @wip - tags := "@all && ~@wip" + tags := "@oidc4vc_rest && ~@wip" if os.Getenv("TAGS") != "" { tags = os.Getenv("TAGS") diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index 7eef3cdc7..b5f89cef5 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -7,78 +7,78 @@ @all @oidc4vc_rest Feature: OIDC4VC REST API - - Scenario Outline: OIDC credential issuance and verification Auth flow - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "" is authorized as a Profile user - And User holds credential "" with templateID "" - - When User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration - Then credential is issued - Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" - And Verifier from organization "test_org" retrieves interactions claims - Then we wait 2 seconds - And Verifier form organization "test_org" requests deleted interactions claims - - Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -# SDJWT issuer, JWT verifier, no limit disclosure in PD query. - | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | -# SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. - | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -# JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. - | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | -# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. - | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | - - Scenario Outline: OIDC credential issuance and verification Pre Auth flow - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "" is authorized as a Profile user - And User holds credential "" with templateID "" - - When User interacts with Wallet to initiate credential issuance using pre authorization code flow - Then credential is issued - Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" - And Verifier from organization "test_org" retrieves interactions claims - Then we wait 2 seconds - And Verifier form organization "test_org" requests deleted interactions claims - - Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -# SDJWT issuer, JWT verifier, no limit disclosure in PD query. - | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | -# SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. - | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -# JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. - | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | -# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. - | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | - -# Error cases - Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Claims) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user - Then User interacts with Wallet to initiate credential issuance using pre authorization code flow with invalid claims - - Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Field in Presentation Definition) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "i_myprofile_ud_es256k_jwt/v1.0" is authorized as a Profile user - And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" - When User interacts with Wallet to initiate credential issuance using pre authorization code flow - Then credential is issued - And User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt/v1.0" profile for organization "test_org" with presentation definition ID "32f54163-no-limit-disclosure-optional-fields" and fields "lpr_category_id,commuter_classification,invalidfield" and receives "field invalidfield not found" error - - Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker stealing auth code & calling token endpoint with it) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user - And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" - Then Malicious attacker stealing auth code from User and using "malicious_attacker_id" ClientID makes /token request and receives "invalid_client" error - - Scenario: OIDC credential issuance and verification Pre Auth flow (issuer has pre-authorized_grant_anonymous_access_supported disabled) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "i_disabled_preauth_without_client_id/v1.0" is authorized as a Profile user - And User holds credential "VerifiedEmployee" with templateID "templateID" - Then User interacts with Wallet to initiate credential issuance using pre authorization code flow and receives "invalid_client" error +# +# Scenario Outline: OIDC credential issuance and verification Auth flow +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "" is authorized as a Profile user +# And User holds credential "" with templateID "" +# +# When User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration +# Then credential is issued +# Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" +# And Verifier from organization "test_org" retrieves interactions claims +# Then we wait 2 seconds +# And Verifier form organization "test_org" requests deleted interactions claims +# +# Examples: +# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +## SDJWT issuer, JWT verifier, no limit disclosure in PD query. +# | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | +## SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. +# | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +## JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. +# | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | +## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. +# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | +# +# Scenario Outline: OIDC credential issuance and verification Pre Auth flow +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "" is authorized as a Profile user +# And User holds credential "" with templateID "" +# +# When User interacts with Wallet to initiate credential issuance using pre authorization code flow +# Then credential is issued +# Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" +# And Verifier from organization "test_org" retrieves interactions claims +# Then we wait 2 seconds +# And Verifier form organization "test_org" requests deleted interactions claims +# +# Examples: +# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +## SDJWT issuer, JWT verifier, no limit disclosure in PD query. +# | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | +## SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. +# | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +## JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. +# | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | +## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. +# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | +# +## Error cases +# Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Claims) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user +# Then User interacts with Wallet to initiate credential issuance using pre authorization code flow with invalid claims +# +# Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Field in Presentation Definition) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "i_myprofile_ud_es256k_jwt/v1.0" is authorized as a Profile user +# And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" +# When User interacts with Wallet to initiate credential issuance using pre authorization code flow +# Then credential is issued +# And User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt/v1.0" profile for organization "test_org" with presentation definition ID "32f54163-no-limit-disclosure-optional-fields" and fields "lpr_category_id,commuter_classification,invalidfield" and receives "field invalidfield not found" error +# +# Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker stealing auth code & calling token endpoint with it) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user +# And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" +# Then Malicious attacker stealing auth code from User and using "malicious_attacker_id" ClientID makes /token request and receives "invalid_client" error +# +# Scenario: OIDC credential issuance and verification Pre Auth flow (issuer has pre-authorized_grant_anonymous_access_supported disabled) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "i_disabled_preauth_without_client_id/v1.0" is authorized as a Profile user +# And User holds credential "VerifiedEmployee" with templateID "templateID" +# Then User interacts with Wallet to initiate credential issuance using pre authorization code flow and receives "invalid_client" error Scenario Outline: OIDC credential issuance and verification Auth flow (Claims Expiry) Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" @@ -97,38 +97,38 @@ Feature: OIDC4VC REST API | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | - Scenario Outline: OIDC credential issuance and verification Pre Auth flow (Limit Disclosures enabled for JWT and LDP VC) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "" is authorized as a Profile user - And User holds credential "" with templateID "" - - When User interacts with Wallet to initiate credential issuance using pre authorization code flow - Then credential is issued - And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "verifiable credential doesn't contains proof" error - Then we wait 15 seconds - And Verifier form organization "test_org" requests expired interactions claims - - Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -# JWT issuer, JWT verifier, limit disclosure enabled in PD query. - | i_myprofile_ud_es256k_jwt/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -# LDP issuer, LDP verifier, limit disclosure enabled in PD query. - | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | - - Scenario Outline: OIDC credential issuance and verification Pre Auth flow (OIDC4VP flow - unsupported vp_token format) - Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" - And Issuer with id "" is authorized as a Profile user - And User holds credential "" with templateID "" - - When User interacts with Wallet to initiate credential issuance using pre authorization code flow - Then credential is issued - And wallet configured to use hardcoded vp_token format "jwt" for OIDC4VP interaction - And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "profile does not support jwt vp_token format" error - Then we wait 15 seconds - And Verifier form organization "test_org" requests expired interactions claims - - Examples: - | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. - | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | - +# Scenario Outline: OIDC credential issuance and verification Pre Auth flow (Limit Disclosures enabled for JWT and LDP VC) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "" is authorized as a Profile user +# And User holds credential "" with templateID "" +# +# When User interacts with Wallet to initiate credential issuance using pre authorization code flow +# Then credential is issued +# And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "verifiable credential doesn't contains proof" error +# Then we wait 15 seconds +# And Verifier form organization "test_org" requests expired interactions claims +# +# Examples: +# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +## JWT issuer, JWT verifier, limit disclosure enabled in PD query. +# | i_myprofile_ud_es256k_jwt/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +## LDP issuer, LDP verifier, limit disclosure enabled in PD query. +# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +# +# Scenario Outline: OIDC credential issuance and verification Pre Auth flow (OIDC4VP flow - unsupported vp_token format) +# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" +# And Issuer with id "" is authorized as a Profile user +# And User holds credential "" with templateID "" +# +# When User interacts with Wallet to initiate credential issuance using pre authorization code flow +# Then credential is issued +# And wallet configured to use hardcoded vp_token format "jwt" for OIDC4VP interaction +# And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "profile does not support jwt vp_token format" error +# Then we wait 15 seconds +# And Verifier form organization "test_org" requests expired interactions claims +# +# Examples: +# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. +# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | +# diff --git a/test/bdd/fixtures/oauth-clients/clients.json b/test/bdd/fixtures/oauth-clients/clients.json index 034c50984..760b6cb41 100644 --- a/test/bdd/fixtures/oauth-clients/clients.json +++ b/test/bdd/fixtures/oauth-clients/clients.json @@ -4,7 +4,11 @@ "redirect_uris": ["http://127.0.0.1/callback"], "grant_types": ["authorization_code"], "response_types": ["code"], - "scopes": ["openid","profile"], + "scopes": [ + "openid", + "profile", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer/v1.0||https://mock-login-consent.example.com:8099/claim-data?credentialType=UniversityDegreeCredential||universityDegreeTemplateID" + ], "token_endpoint_auth_method": "none" } ] \ No newline at end of file diff --git a/test/bdd/fixtures/profile/profiles.json b/test/bdd/fixtures/profile/profiles.json index ac1e06874..74a8f7355 100644 --- a/test/bdd/fixtures/profile/profiles.json +++ b/test/bdd/fixtures/profile/profiles.json @@ -689,7 +689,8 @@ "none" ], "enable_dynamic_client_registration": true, - "pre-authorized_grant_anonymous_access_supported": true + "pre-authorized_grant_anonymous_access_supported": true, + "wallet_initiated_auth_flow_supported": true }, "credentialTemplates": [ { diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go index 4492f8383..a678857f6 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go @@ -34,6 +34,7 @@ const ( initiateCredentialIssuanceURLFormat = vcsAPIGateway + "/issuer/profiles/%s/%s/interactions/initiate-oidc" vcsAuthorizeEndpoint = vcsAPIGateway + "/oidc/authorize" vcsTokenEndpoint = vcsAPIGateway + "/oidc/token" + vcsIssuerURL = vcsAPIGateway + "/issuer/%s/%s" oidcProviderURL = "http://cognito-mock.trustbloc.local:9229/local_5a9GzRvB" loginPageURL = "https://localhost:8099/login" claimDataURL = "https://mock-login-consent.example.com:8099/claim-data" @@ -295,6 +296,29 @@ func (s *Steps) runOIDC4CIAuth() error { return nil } +func (s *Steps) runOIDC4CIAuthWalletInitiatedFlow() error { + walletInitiatedFlowScope := fmt.Sprintf("%s||%s||%s", + fmt.Sprintf(vcsIssuerURL, s.issuerProfile.ID, s.issuerProfile.Version), + claimDataURL+"?credentialType="+s.issuedCredentialType, + s.issuedCredentialTemplateID, + ) + + err := s.walletRunner.RunOIDC4CIWalletInitiated(&walletrunner.OIDC4CIConfig{ + ClientID: "oidc4vc_client", + Scope: []string{"openid", "profile", walletInitiatedFlowScope}, + RedirectURI: "http://127.0.0.1/callback", + CredentialType: s.issuedCredentialType, + CredentialFormat: s.issuerProfile.CredentialMetaData.CredentialsSupported[0]["format"].(string), + Login: "bdd-test", + Password: "bdd-test-pass", + }, nil) + if err != nil { + return fmt.Errorf("s.walletRunner.RunOIDC4CIWalletInitiated: %w", err) + } + + return nil +} + func (s *Steps) runOIDC4CIAuthWithDynamicClient() error { initiateOIDC4CIResponseData, err := s.initiateCredentialIssuance(s.getInitiateIssuanceRequest()) if err != nil { diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 16bc4a54a..e320c220c 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -88,7 +88,7 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^credential is issued$`, s.checkIssuedCredential) // CI. - sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow$`, s.runOIDC4CIAuth) + sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow$`, s.runOIDC4CIAuthWalletInitiatedFlow) sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration$`, s.runOIDC4CIAuthWithDynamicClient) sc.Step(`^User interacts with Wallet to initiate credential issuance using pre authorization code flow$`, s.runOIDC4CIPreAuthWithValidClaims) From f066e1a615ed9f8987a682fa4fd7fec64df314dc Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 12:34:50 +0200 Subject: [PATCH 02/22] chore: use only issuer url Signed-off-by: Stas D --- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 38 ++- pkg/kms/mocks/kms_mocks.go | 112 ++++++++- pkg/service/oidc4ci/oidc4ci_service.go | 92 ++++---- pkg/service/oidc4ci/regex.go | 5 + pkg/service/oidc4ci/regexp_test.go | 40 ++++ test/bdd/features/oidc4vc_api.feature | 216 +++++++++--------- test/bdd/fixtures/oauth-clients/clients.json | 4 +- test/bdd/fixtures/profile/profiles.json | 4 +- test/bdd/pkg/v1/oidc4vc/oidc4ci.go | 6 +- test/bdd/pkg/v1/oidc4vc/steps.go | 1 + 10 files changed, 326 insertions(+), 192 deletions(-) create mode 100644 pkg/service/oidc4ci/regex.go create mode 100644 pkg/service/oidc4ci/regexp_test.go diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index b64e15514..f9a1ec641 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -17,6 +17,7 @@ import ( "net" "net/http" "net/url" + "regexp" "strings" "time" @@ -35,18 +36,13 @@ import ( "github.com/trustbloc/vcs/pkg/kms/signer" "github.com/trustbloc/vcs/pkg/restapi/v1/common" issuerv1 "github.com/trustbloc/vcs/pkg/restapi/v1/issuer" + "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) const ( jwtProofTypHeader = "openid4vci-proof+jwt" ) -type walletInitiatedFlowParams struct { - credentialIssuer string // IssuerURL - claimsEndpoint string - credentialTemplateID string -} - type OIDC4CIConfig struct { InitiateIssuanceURL string ClientID string @@ -236,42 +232,38 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { return nil } -func extractWalletInitiatedFlowParams(scopes []string) *walletInitiatedFlowParams { - for _, scope := range scopes { - chunks := strings.Split(scope, "||") - if len(chunks) != 3 { - continue - } +var matchRegex = regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex) - return &walletInitiatedFlowParams{ - credentialIssuer: chunks[0], - claimsEndpoint: chunks[1], - credentialTemplateID: chunks[2], +func extractIssuerUrlFromScopes(scopes []string) (string, error) { + for _, scope := range scopes { + if matchRegex.MatchString(scope) { + return scope, nil } } - return nil + return "", errors.New("issuer url not found in scopes") } func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow Wallet initiated") // Check whether scope contains combined string Issuer URL||ClaimEndpoint||credentialTemplateID - walletInitiatedFlowData := extractWalletInitiatedFlowParams(config.Scope) - if walletInitiatedFlowData == nil { + issuerUrl, err := extractIssuerUrlFromScopes(config.Scope) + if err != nil { return errors.New( "undefined scopes supplied. " + - "Make sure one of the provided scope is a combined string ||||") + "Make sure one of the provided scope is an vcs issuer url format. ref " + + oidc4ci.WalletInitFlowClaimRegex) } oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig( - walletInitiatedFlowData.credentialIssuer, + issuerUrl, ) if err != nil { return fmt.Errorf("get issuer oidc issuer config: %w", err) } - oidcConfig, err := s.getIssuerOIDCConfig(walletInitiatedFlowData.credentialIssuer) + oidcConfig, err := s.getIssuerOIDCConfig(issuerUrl) if err != nil { return fmt.Errorf("get issuer oidc config: %w", err) } @@ -379,7 +371,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) oidcIssuerCredentialConfig.CredentialEndpoint, config.CredentialType, config.CredentialFormat, - walletInitiatedFlowData.credentialIssuer, + issuerUrl, ) if err != nil { return fmt.Errorf("get credential: %w", err) diff --git a/pkg/kms/mocks/kms_mocks.go b/pkg/kms/mocks/kms_mocks.go index fe0c50b88..4da155666 100644 --- a/pkg/kms/mocks/kms_mocks.go +++ b/pkg/kms/mocks/kms_mocks.go @@ -48,9 +48,33 @@ func (m *MockVCSKeyManager) CreateCryptoKey(keyType kms.KeyType) (string, interf } // CreateCryptoKey indicates an expected call of CreateCryptoKey. -func (mr *MockVCSKeyManagerMockRecorder) CreateCryptoKey(keyType interface{}) *gomock.Call { +func (mr *MockVCSKeyManagerMockRecorder) CreateCryptoKey(keyType interface{}) *VCSKeyManagerCreateCryptoKeyCall { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCryptoKey", reflect.TypeOf((*MockVCSKeyManager)(nil).CreateCryptoKey), keyType) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCryptoKey", reflect.TypeOf((*MockVCSKeyManager)(nil).CreateCryptoKey), keyType) + return &VCSKeyManagerCreateCryptoKeyCall{Call: call} +} + +// VCSKeyManagerCreateCryptoKeyCall wrap *gomock.Call +type VCSKeyManagerCreateCryptoKeyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *VCSKeyManagerCreateCryptoKeyCall) Return(arg0 string, arg1 interface{}, arg2 error) *VCSKeyManagerCreateCryptoKeyCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *VCSKeyManagerCreateCryptoKeyCall) Do(f func(kms.KeyType) (string, interface{}, error)) *VCSKeyManagerCreateCryptoKeyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *VCSKeyManagerCreateCryptoKeyCall) DoAndReturn(f func(kms.KeyType) (string, interface{}, error)) *VCSKeyManagerCreateCryptoKeyCall { + c.Call = c.Call.DoAndReturn(f) + return c } // CreateJWKKey mocks base method. @@ -64,9 +88,33 @@ func (m *MockVCSKeyManager) CreateJWKKey(keyType kms.KeyType) (string, *jwk.JWK, } // CreateJWKKey indicates an expected call of CreateJWKKey. -func (mr *MockVCSKeyManagerMockRecorder) CreateJWKKey(keyType interface{}) *gomock.Call { +func (mr *MockVCSKeyManagerMockRecorder) CreateJWKKey(keyType interface{}) *VCSKeyManagerCreateJWKKeyCall { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJWKKey", reflect.TypeOf((*MockVCSKeyManager)(nil).CreateJWKKey), keyType) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJWKKey", reflect.TypeOf((*MockVCSKeyManager)(nil).CreateJWKKey), keyType) + return &VCSKeyManagerCreateJWKKeyCall{Call: call} +} + +// VCSKeyManagerCreateJWKKeyCall wrap *gomock.Call +type VCSKeyManagerCreateJWKKeyCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *VCSKeyManagerCreateJWKKeyCall) Return(arg0 string, arg1 *jwk.JWK, arg2 error) *VCSKeyManagerCreateJWKKeyCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *VCSKeyManagerCreateJWKKeyCall) Do(f func(kms.KeyType) (string, *jwk.JWK, error)) *VCSKeyManagerCreateJWKKeyCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *VCSKeyManagerCreateJWKKeyCall) DoAndReturn(f func(kms.KeyType) (string, *jwk.JWK, error)) *VCSKeyManagerCreateJWKKeyCall { + c.Call = c.Call.DoAndReturn(f) + return c } // NewVCSigner mocks base method. @@ -79,9 +127,33 @@ func (m *MockVCSKeyManager) NewVCSigner(creator string, signatureType verifiable } // NewVCSigner indicates an expected call of NewVCSigner. -func (mr *MockVCSKeyManagerMockRecorder) NewVCSigner(creator, signatureType interface{}) *gomock.Call { +func (mr *MockVCSKeyManagerMockRecorder) NewVCSigner(creator, signatureType interface{}) *VCSKeyManagerNewVCSignerCall { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewVCSigner", reflect.TypeOf((*MockVCSKeyManager)(nil).NewVCSigner), creator, signatureType) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewVCSigner", reflect.TypeOf((*MockVCSKeyManager)(nil).NewVCSigner), creator, signatureType) + return &VCSKeyManagerNewVCSignerCall{Call: call} +} + +// VCSKeyManagerNewVCSignerCall wrap *gomock.Call +type VCSKeyManagerNewVCSignerCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *VCSKeyManagerNewVCSignerCall) Return(arg0 vc.SignerAlgorithm, arg1 error) *VCSKeyManagerNewVCSignerCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *VCSKeyManagerNewVCSignerCall) Do(f func(string, verifiable.SignatureType) (vc.SignerAlgorithm, error)) *VCSKeyManagerNewVCSignerCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *VCSKeyManagerNewVCSignerCall) DoAndReturn(f func(string, verifiable.SignatureType) (vc.SignerAlgorithm, error)) *VCSKeyManagerNewVCSignerCall { + c.Call = c.Call.DoAndReturn(f) + return c } // SupportedKeyTypes mocks base method. @@ -93,7 +165,31 @@ func (m *MockVCSKeyManager) SupportedKeyTypes() []kms.KeyType { } // SupportedKeyTypes indicates an expected call of SupportedKeyTypes. -func (mr *MockVCSKeyManagerMockRecorder) SupportedKeyTypes() *gomock.Call { +func (mr *MockVCSKeyManagerMockRecorder) SupportedKeyTypes() *VCSKeyManagerSupportedKeyTypesCall { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedKeyTypes", reflect.TypeOf((*MockVCSKeyManager)(nil).SupportedKeyTypes)) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedKeyTypes", reflect.TypeOf((*MockVCSKeyManager)(nil).SupportedKeyTypes)) + return &VCSKeyManagerSupportedKeyTypesCall{Call: call} +} + +// VCSKeyManagerSupportedKeyTypesCall wrap *gomock.Call +type VCSKeyManagerSupportedKeyTypesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *VCSKeyManagerSupportedKeyTypesCall) Return(arg0 []kms.KeyType) *VCSKeyManagerSupportedKeyTypesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *VCSKeyManagerSupportedKeyTypesCall) Do(f func() []kms.KeyType) *VCSKeyManagerSupportedKeyTypesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *VCSKeyManagerSupportedKeyTypesCall) DoAndReturn(f func() []kms.KeyType) *VCSKeyManagerSupportedKeyTypesCall { + c.Call = c.Call.DoAndReturn(f) + return c } diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index eb28c9c2b..940729210 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -14,6 +14,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "strings" "time" @@ -168,16 +169,21 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( req *PrepareClaimDataAuthorizationRequest, ) (*PrepareClaimDataAuthorizationResponse, error) { tx, err := s.store.FindByOpState(ctx, req.OpState) + if err != nil { - if errors.Is(err, ErrDataNotFound) { - // check if issuer supports Wallet initiated flow - if walletInitiatedFlowScopeData := s.isWalletInitiatedFlow(req.Scope); walletInitiatedFlowScopeData != nil { - // process wallet initiated flow - return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, walletInitiatedFlowScopeData) - } + if !errors.Is(err, ErrDataNotFound) { + return nil, err + } + + // check if issuer supports Wallet initiated flow + issuerURL := s.extractIssuerURLFromClaims(req.Scope) + if issuerURL == "" { // otherwise - return regular error + return nil, err } - return nil, fmt.Errorf("find tx by op state: %w", err) + + // process wallet initiated flow + return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, issuerURL) } newState := TransactionStateAwaitingIssuerOIDCAuthorization @@ -266,58 +272,39 @@ func (s *Service) getIssuerOIDCConfig( return &oidcConfig, nil } -type walletInitiatedFlowScopeParsed struct { - scope string - issuerURL string - claimsEndpoint string - credentialTemplateID string -} +func (s *Service) extractIssuerURLFromClaims(requestScopes []string) string { + matchRegex := regexp.MustCompile(WalletInitFlowClaimRegex) -func (s *Service) isWalletInitiatedFlow(requestScopes []string) *walletInitiatedFlowScopeParsed { for _, scope := range requestScopes { - //if parsedURL, err := url.Parse(scope); err != nil || parsedURL.Host == "" { - // continue - //} - chunks := strings.Split(scope, "||") - if len(chunks) != 3 { - continue - } - - //todo: make a call to well-known. - //issuerOidcConfiguration, err := s.getIssuerOIDCConfig(chunks[0]) - //if err != nil { - // logger.Error("getIssuerOIDCConfig", log.WithError(err)) - // return nil - //} - // - //fmt.Println(issuerOidcConfiguration) - //if issuerOidcConfiguration.WalletInitiatedAuthFlowSupported { - if true { - return &walletInitiatedFlowScopeParsed{ - scope: scope, - issuerURL: chunks[0], - claimsEndpoint: chunks[1], - credentialTemplateID: chunks[2], - } + if matchRegex.MatchString(scope) { + return scope } } - return nil + return "" } func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( ctx context.Context, requestScopes []string, - walletInitiatedFlowScopeData *walletInitiatedFlowScopeParsed) (*PrepareClaimDataAuthorizationResponse, error) { - // todo: temp solution until will find a better way to pass profile ID/Version - chunks := strings.Split(walletInitiatedFlowScopeData.issuerURL, "/") - profileID, profileVersion := chunks[len(chunks)-2], chunks[len(chunks)-1] + issuerURL string, +) (*PrepareClaimDataAuthorizationResponse, error) { + matches := regexp.MustCompile(WalletInitFlowClaimRegex).FindStringSubmatch(issuerURL) + if len(matches) != 4 { + logger.Error("invalid issuer url for wallet initiated flow", log.WithURL(issuerURL)) + return nil, errors.New("invalid issuer url") + } + profileID, profileVersion := matches[2], matches[3] profile, err := s.profileService.GetProfile(profileID, profileVersion) if err != nil { return nil, fmt.Errorf("wallet initiated flow get profile: %w", err) } + if !profile.OIDCConfig.WalletInitiatedAuthFlowSupported { + return nil, errors.New("wallet initiated auth flow is not supported for current profile") + } + oidcConfig, err := s.wellKnownService.GetOIDCConfiguration(ctx, profile.OIDCConfig.IssuerWellKnownURL) if err != nil { return nil, fmt.Errorf("wallet initiated flow get oidc config: %w", err) @@ -327,7 +314,7 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( scopes := make([]string, 0, len(requestScopes)-1) for _, scope := range requestScopes { - if scope == walletInitiatedFlowScopeData.scope { + if scope == issuerURL { continue } scopes = append(scopes, scope) @@ -347,12 +334,25 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( return nil, err } + var credTemplate string // todo Sudesh remove hardcode + var credType string + if profile.ID == "bank_issuer" { + credTemplate = "universityDegreeTemplateID" + credType = "UniversityDegreeCredential" + } else if profile.ID == "i_myprofile_ud_es256k_jwt" { + credTemplate = "permanentResidentCardTemplateID" + credType = "PermanentResidentCard" + } else { + credTemplate = "crudeProductCredentialTemplateID" + credType = "CrudeProductCredential" + } + return &PrepareClaimDataAuthorizationResponse{ WalletInitiatedFlow: &WalletInitiatedFlow{ ProfileID: profileID, ProfileVersion: profileVersion, - ClaimEndpoint: walletInitiatedFlowScopeData.claimsEndpoint, - CredentialTemplateID: walletInitiatedFlowScopeData.credentialTemplateID, + ClaimEndpoint: fmt.Sprintf("https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), // todo Sudesh remove hardcode + CredentialTemplateID: credTemplate, // todo Sudesh remove hardcode }, ProfileID: profileID, ProfileVersion: profileVersion, diff --git a/pkg/service/oidc4ci/regex.go b/pkg/service/oidc4ci/regex.go new file mode 100644 index 000000000..23b26dd5d --- /dev/null +++ b/pkg/service/oidc4ci/regex.go @@ -0,0 +1,5 @@ +package oidc4ci + +const ( + WalletInitFlowClaimRegex = `(https|http):\/\/.*\/issuer\/(.*?)\/(.*)$` +) diff --git a/pkg/service/oidc4ci/regexp_test.go b/pkg/service/oidc4ci/regexp_test.go new file mode 100644 index 000000000..a040722bf --- /dev/null +++ b/pkg/service/oidc4ci/regexp_test.go @@ -0,0 +1,40 @@ +package oidc4ci_test + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/trustbloc/vcs/pkg/service/oidc4ci" +) + +func TestRegex(t *testing.T) { + testCases := []struct { + Input string + Version string + ProfileID string + }{ + { + Input: "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer/v1.0", + Version: "v1.0", + ProfileID: "bank_issuer", + }, + { + Input: "https://api-gateway.trustbloc.local:5566/issuer/some-issuer/latest", + Version: "latest", + ProfileID: "some-issuer", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Input, func(t *testing.T) { + rg := regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex) + matches := rg.FindStringSubmatch(testCase.Input) + profileID, profileVersion := matches[2], matches[3] + + assert.Equal(t, testCase.ProfileID, profileID) + assert.Equal(t, testCase.Version, profileVersion) + }) + } +} diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index b5f89cef5..87f885d0b 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -7,78 +7,78 @@ @all @oidc4vc_rest Feature: OIDC4VC REST API -# -# Scenario Outline: OIDC credential issuance and verification Auth flow -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "" is authorized as a Profile user -# And User holds credential "" with templateID "" -# -# When User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration -# Then credential is issued -# Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" -# And Verifier from organization "test_org" retrieves interactions claims -# Then we wait 2 seconds -# And Verifier form organization "test_org" requests deleted interactions claims -# -# Examples: -# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -## SDJWT issuer, JWT verifier, no limit disclosure in PD query. -# | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | -## SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. -# | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -## JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. -# | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | -## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. -# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | -# -# Scenario Outline: OIDC credential issuance and verification Pre Auth flow -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "" is authorized as a Profile user -# And User holds credential "" with templateID "" -# -# When User interacts with Wallet to initiate credential issuance using pre authorization code flow -# Then credential is issued -# Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" -# And Verifier from organization "test_org" retrieves interactions claims -# Then we wait 2 seconds -# And Verifier form organization "test_org" requests deleted interactions claims -# -# Examples: -# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -## SDJWT issuer, JWT verifier, no limit disclosure in PD query. -# | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | -## SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. -# | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -## JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. -# | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | -## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. -# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | -# -## Error cases -# Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Claims) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user -# Then User interacts with Wallet to initiate credential issuance using pre authorization code flow with invalid claims -# -# Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Field in Presentation Definition) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "i_myprofile_ud_es256k_jwt/v1.0" is authorized as a Profile user -# And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" -# When User interacts with Wallet to initiate credential issuance using pre authorization code flow -# Then credential is issued -# And User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt/v1.0" profile for organization "test_org" with presentation definition ID "32f54163-no-limit-disclosure-optional-fields" and fields "lpr_category_id,commuter_classification,invalidfield" and receives "field invalidfield not found" error -# -# Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker stealing auth code & calling token endpoint with it) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user -# And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" -# Then Malicious attacker stealing auth code from User and using "malicious_attacker_id" ClientID makes /token request and receives "invalid_client" error -# -# Scenario: OIDC credential issuance and verification Pre Auth flow (issuer has pre-authorized_grant_anonymous_access_supported disabled) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "i_disabled_preauth_without_client_id/v1.0" is authorized as a Profile user -# And User holds credential "VerifiedEmployee" with templateID "templateID" -# Then User interacts with Wallet to initiate credential issuance using pre authorization code flow and receives "invalid_client" error + + Scenario Outline: OIDC credential issuance and verification Auth flow + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" + + When User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration + Then credential is issued + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" + And Verifier from organization "test_org" retrieves interactions claims + Then we wait 2 seconds + And Verifier form organization "test_org" requests deleted interactions claims + + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +# SDJWT issuer, JWT verifier, no limit disclosure in PD query. + | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | +# SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. + | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +# JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. + | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | +# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. + | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | + + Scenario Outline: OIDC credential issuance and verification Pre Auth flow + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" + + When User interacts with Wallet to initiate credential issuance using pre authorization code flow + Then credential is issued + Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" + And Verifier from organization "test_org" retrieves interactions claims + Then we wait 2 seconds + And Verifier form organization "test_org" requests deleted interactions claims + + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +# SDJWT issuer, JWT verifier, no limit disclosure in PD query. + | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | +# SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query. + | bank_issuer/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +# JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query. + | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | +# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. + | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | + +# Error cases + Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Claims) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user + Then User interacts with Wallet to initiate credential issuance using pre authorization code flow with invalid claims + + Scenario: OIDC credential issuance and verification Pre Auth flow (Invalid Field in Presentation Definition) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "i_myprofile_ud_es256k_jwt/v1.0" is authorized as a Profile user + And User holds credential "CrudeProductCredential" with templateID "crudeProductCredentialTemplateID" + When User interacts with Wallet to initiate credential issuance using pre authorization code flow + Then credential is issued + And User interacts with Verifier and initiate OIDC4VP interaction under "v_myprofile_jwt/v1.0" profile for organization "test_org" with presentation definition ID "32f54163-no-limit-disclosure-optional-fields" and fields "lpr_category_id,commuter_classification,invalidfield" and receives "field invalidfield not found" error + + Scenario: OIDC credential issuance and verification Auth flow (Malicious attacker stealing auth code & calling token endpoint with it) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "bank_issuer/v1.0" is authorized as a Profile user + And User holds credential "UniversityDegreeCredential" with templateID "universityDegreeTemplateID" + Then Malicious attacker stealing auth code from User and using "malicious_attacker_id" ClientID makes /token request and receives "invalid_client" error + + Scenario: OIDC credential issuance and verification Pre Auth flow (issuer has pre-authorized_grant_anonymous_access_supported disabled) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "i_disabled_preauth_without_client_id/v1.0" is authorized as a Profile user + And User holds credential "VerifiedEmployee" with templateID "templateID" + Then User interacts with Wallet to initiate credential issuance using pre authorization code flow and receives "invalid_client" error Scenario Outline: OIDC credential issuance and verification Auth flow (Claims Expiry) Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" @@ -94,41 +94,41 @@ Feature: OIDC4VC REST API Examples: | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | | bank_issuer/v1.0 | UniversityDegreeCredential | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id | - | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,commuter_classification,registration_city | + | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification | | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | -# Scenario Outline: OIDC credential issuance and verification Pre Auth flow (Limit Disclosures enabled for JWT and LDP VC) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "" is authorized as a Profile user -# And User holds credential "" with templateID "" -# -# When User interacts with Wallet to initiate credential issuance using pre authorization code flow -# Then credential is issued -# And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "verifiable credential doesn't contains proof" error -# Then we wait 15 seconds -# And Verifier form organization "test_org" requests expired interactions claims -# -# Examples: -# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -## JWT issuer, JWT verifier, limit disclosure enabled in PD query. -# | i_myprofile_ud_es256k_jwt/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -## LDP issuer, LDP verifier, limit disclosure enabled in PD query. -# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | -# -# Scenario Outline: OIDC credential issuance and verification Pre Auth flow (OIDC4VP flow - unsupported vp_token format) -# Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" -# And Issuer with id "" is authorized as a Profile user -# And User holds credential "" with templateID "" -# -# When User interacts with Wallet to initiate credential issuance using pre authorization code flow -# Then credential is issued -# And wallet configured to use hardcoded vp_token format "jwt" for OIDC4VP interaction -# And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "profile does not support jwt vp_token format" error -# Then we wait 15 seconds -# And Verifier form organization "test_org" requests expired interactions claims -# -# Examples: -# | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | -## LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. -# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | -# + Scenario Outline: OIDC credential issuance and verification Pre Auth flow (Limit Disclosures enabled for JWT and LDP VC) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" + + When User interacts with Wallet to initiate credential issuance using pre authorization code flow + Then credential is issued + And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "verifiable credential doesn't contains proof" error + Then we wait 15 seconds + And Verifier form organization "test_org" requests expired interactions claims + + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +# JWT issuer, JWT verifier, limit disclosure enabled in PD query. + | i_myprofile_ud_es256k_jwt/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | +# LDP issuer, LDP verifier, limit disclosure enabled in PD query. + | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address | + + Scenario Outline: OIDC credential issuance and verification Pre Auth flow (OIDC4VP flow - unsupported vp_token format) + Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" + And Issuer with id "" is authorized as a Profile user + And User holds credential "" with templateID "" + + When User interacts with Wallet to initiate credential issuance using pre authorization code flow + Then credential is issued + And wallet configured to use hardcoded vp_token format "jwt" for OIDC4VP interaction + And User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" and receives "profile does not support jwt vp_token format" error + Then we wait 15 seconds + And Verifier form organization "test_org" requests expired interactions claims + + Examples: + | issuerProfile | credentialType | credentialTemplate | verifierProfile | presentationDefinitionID | fields | +# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query. + | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id | + diff --git a/test/bdd/fixtures/oauth-clients/clients.json b/test/bdd/fixtures/oauth-clients/clients.json index 760b6cb41..d36702f2f 100644 --- a/test/bdd/fixtures/oauth-clients/clients.json +++ b/test/bdd/fixtures/oauth-clients/clients.json @@ -7,7 +7,9 @@ "scopes": [ "openid", "profile", - "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer/v1.0||https://mock-login-consent.example.com:8099/claim-data?credentialType=UniversityDegreeCredential||universityDegreeTemplateID" + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer/v1.0", + "https://api-gateway.trustbloc.local:5566/issuer/i_myprofile_cmtr_p256_ldp/v1.0", + "https://api-gateway.trustbloc.local:5566/issuer/i_myprofile_ud_es256k_jwt/v1.0" ], "token_endpoint_auth_method": "none" } diff --git a/test/bdd/fixtures/profile/profiles.json b/test/bdd/fixtures/profile/profiles.json index 74a8f7355..4d2dfba9e 100644 --- a/test/bdd/fixtures/profile/profiles.json +++ b/test/bdd/fixtures/profile/profiles.json @@ -166,6 +166,7 @@ "none" ], "enable_dynamic_client_registration": true, + "wallet_initiated_auth_flow_supported": true, "pre-authorized_grant_anonymous_access_supported": true }, "credentialTemplates": [ @@ -512,7 +513,8 @@ "none" ], "enable_dynamic_client_registration": true, - "pre-authorized_grant_anonymous_access_supported": true + "pre-authorized_grant_anonymous_access_supported": true, + "wallet_initiated_auth_flow_supported": true }, "credentialTemplates": [ { diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go index a678857f6..ed826e8e4 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go @@ -297,11 +297,7 @@ func (s *Steps) runOIDC4CIAuth() error { } func (s *Steps) runOIDC4CIAuthWalletInitiatedFlow() error { - walletInitiatedFlowScope := fmt.Sprintf("%s||%s||%s", - fmt.Sprintf(vcsIssuerURL, s.issuerProfile.ID, s.issuerProfile.Version), - claimDataURL+"?credentialType="+s.issuedCredentialType, - s.issuedCredentialTemplateID, - ) + walletInitiatedFlowScope := fmt.Sprintf(vcsIssuerURL, s.issuerProfile.ID, s.issuerProfile.Version) err := s.walletRunner.RunOIDC4CIWalletInitiated(&walletrunner.OIDC4CIConfig{ ClientID: "oidc4vc_client", diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index e320c220c..9d4b76388 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -62,6 +62,7 @@ func NewSteps(ctx *bddcontext.BDDContext) (*Steps, error) { c.DidKeyType = "ECDSAP384DER" c.DidMethod = "orb" c.KeepWalletOpen = true + c.Debug = true }) if err != nil { return nil, fmt.Errorf("unable create wallet runner: %w", err) From 40cee43b6d290a924f8c04234b212c9f0a943315 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 13:42:08 +0200 Subject: [PATCH 03/22] refactor: wallet flow Signed-off-by: Stas D --- api/spec/openapi.gen.go | 272 +++++++++--------- docs/v1/common.yaml | 22 ++ docs/v1/openapi.yaml | 36 +-- .../wrappers/oidc4ci/oidc4ci_wrapper.go | 5 +- pkg/restapi/v1/common/common.go | 28 -- pkg/restapi/v1/common/openapi.gen.go | 44 +-- pkg/restapi/v1/issuer/controller.go | 33 +-- pkg/restapi/v1/issuer/openapi.gen.go | 206 +------------ pkg/restapi/v1/oidc4ci/controller.go | 107 +++---- pkg/restapi/v1/util/validate.go | 41 +++ pkg/service/oidc4ci/api.go | 23 +- pkg/service/oidc4ci/oidc4ci_service.go | 21 +- .../oidc4ci_service_store_auth_code.go | 2 + pkg/service/oidc4ci/util_test.go | 6 + 14 files changed, 324 insertions(+), 522 deletions(-) create mode 100644 pkg/restapi/v1/util/validate.go diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 790872b23..9e8b916a9 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,143 +19,141 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AP//L1a6WgzCAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/docs/v1/common.yaml b/docs/v1/common.yaml index e82a27e53..72f914b53 100644 --- a/docs/v1/common.yaml +++ b/docs/v1/common.yaml @@ -67,6 +67,28 @@ components: - orb - web - key + WalletInitiatedFlowData: + title: WalletInitiatedFlowData + type: object + properties: + profile_id: + type: string + profile_version: + type: string + scopes: + type: array + items: + type: string + claim_endpoint: + type: string + credential_template_id: + type: string + required: + - profile_id + - profile_version + - scopes + - claim_endpoint + - credential_template_id AuthorizationDetails: title: AuthorizationDetails type: object diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 43aadb967..3d567bc90 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -190,38 +190,6 @@ paths: $ref: '#/components/schemas/InitiateOIDC4CIRequest' tags: - issuer - '/issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal': - parameters: - - schema: - type: string - name: profileID - in: path - required: true - description: Issuer Profile ID. - - schema: - type: string - name: profileVersion - in: path - required: true - description: Issuer Profile Version. - post: - summary: Initiate OIDC Credential Issuance - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/InitiateOIDC4CIResponse' - operationId: initiate-credential-issuance-internal - description: Internal endpoint used by Wallet to initiate OIDCI credential issuance interaction. - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/InitiateOIDC4CIRequest' - tags: - - issuer /issuer/interactions/push-authorization-request: post: summary: Push Authorization Details @@ -1190,6 +1158,8 @@ components: type: string code: type: string + wallet_initiated_flow: + $ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData required: - op_state - code @@ -1261,7 +1231,7 @@ components: type: string description: Transaction ID to correlate upcoming authorization response. wallet_initiated_flow: - $ref: '#/components/schemas/WalletInitiatedFlowParameters' + $ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData required: - authorization_request - authorization_endpoint diff --git a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go index cea6c1d66..281f46a5f 100644 --- a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go +++ b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper.go @@ -18,6 +18,7 @@ import ( "github.com/trustbloc/vcs/pkg/observability/tracing/attributeutil" profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) @@ -65,8 +66,8 @@ func (w *Wrapper) PrepareClaimDataAuthorizationRequest(ctx context.Context, req return w.svc.PrepareClaimDataAuthorizationRequest(ctx, req) } -func (w *Wrapper) StoreAuthorizationCode(ctx context.Context, opState string, code string) (oidc4ci.TxID, error) { - return w.svc.StoreAuthorizationCode(ctx, opState, code) +func (w *Wrapper) StoreAuthorizationCode(ctx context.Context, opState string, code string, flowData *common.WalletInitiatedFlowData) (oidc4ci.TxID, error) { + return w.svc.StoreAuthorizationCode(ctx, opState, code, flowData) } func (w *Wrapper) ExchangeAuthorizationCode(ctx context.Context, opState string) (oidc4ci.TxID, error) { diff --git a/pkg/restapi/v1/common/common.go b/pkg/restapi/v1/common/common.go index 8666de424..9d5c67829 100644 --- a/pkg/restapi/v1/common/common.go +++ b/pkg/restapi/v1/common/common.go @@ -9,16 +9,12 @@ SPDX-License-Identifier: Apache-2.0 package common import ( - "errors" "fmt" - "github.com/samber/lo" - vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/kms" profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" - "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) const ( @@ -171,27 +167,3 @@ func MapToKMSConfigType(kmsType kms.Type) (KMSConfigType, error) { fmt.Errorf("kms type missmatch %s, rest api supportes only [%s, %s, %s]", kmsType, KMSConfigTypeAws, KMSConfigTypeLocal, KMSConfigTypeWeb) } - -func ValidateAuthorizationDetails(ad *AuthorizationDetails) (*oidc4ci.AuthorizationDetails, error) { - if ad.Type != "openid_credential" { - return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.type", - errors.New("type should be 'openid_credential'")) - } - - mapped := &oidc4ci.AuthorizationDetails{ - Type: ad.Type, - Types: ad.Types, - Locations: lo.FromPtr(ad.Locations), - } - - if ad.Format != nil { - vcFormat, err := ValidateVCFormat(VCFormat(*ad.Format)) - if err != nil { - return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.format", err) - } - - mapped.Format = vcFormat - } - - return mapped, nil -} diff --git a/pkg/restapi/v1/common/openapi.gen.go b/pkg/restapi/v1/common/openapi.gen.go index c170f95fd..bace32cf9 100644 --- a/pkg/restapi/v1/common/openapi.gen.go +++ b/pkg/restapi/v1/common/openapi.gen.go @@ -90,26 +90,36 @@ type VCFormat string // Supported VP formats. type VPFormat string +// WalletInitiatedFlowData defines model for WalletInitiatedFlowData. +type WalletInitiatedFlowData struct { + ClaimEndpoint string `json:"claim_endpoint"` + CredentialTemplateId string `json:"credential_template_id"` + ProfileId string `json:"profile_id"` + ProfileVersion string `json:"profile_version"` + Scopes []string `json:"scopes"` +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5xWT2/buBP9KgOeWkBxgl9/J9+ychcwkmyNunEP2yKgyJHFmCK15MiKt8h3XwxlxU4s", - "L9q9BBH/vHnz5nHGP4TydeMdOopi+kNEVWEt07/XLVU+mL8lGe9mSNLYtK4xqmAaXhVTcec1WiAPyrst", - "7oAqBN0fBln4ltJKHlCjIyNt7L+tQUfQSUeRL/uCpHETkYkm+AYDGUyxSh9qSadRlxSMW0PAJmBkYLcG", - "Cf1pMA66yqjqTWQwEQL+1WIk1By0QDAxtqgnsJLWaNhK22IEjaVxqKHYwaf5LP//KgcZEB47etiqh8fo", - "3YXVIJ0Gq5uHrZrAnGECKOkgYNlGTKHlsYBDaDBl2lybLTpQB3a0azCB7rNQvi48c3aeILZN4wOhZon4", - "pJiKmDQQz5mwXqUYI+W5diBDkDvwJfQXuACSQFrruwgSVF8K8hAbVKbsSzhA8j3+Dhh9GxRCxLDF8C6+", - "7xFYeN5/ZRZYpkOMWRtHIFtt0KmEQsEo1l8qhZFrv0EXOStDWKcETtLbL6Q8Dt9nLJGS00gYauMwjhRi", - "cCfDTODufvmFnRAxafBN+Aad0Q+HynwTXJLBCqMF4IV4llJfAI3O0yBXunDQdjDlway/oshzJhjCBNRi", - "+me/OXD6ngkyZPn06Ht+wfLFIypi8Nl8dodUeX2a0Gw+gzrtDdx5pX9KbcTkXYhm7Yxbcwbo2pop+VCI", - "THTIfze4S6ze5nRzt8y9K836XI9h7Ju7JTea0qzbkPI4bRm6WAQszdMpTL/OzLUkWci4J13skt0tbOo4", - "Wl5dfBm1HK/+J7j7z7enaPefb1nKXwRDpxtv3EiPZK2G3dGrEVVAuvVqc4O7haRqRDJJVWoN6ShT2fwk", - "L/pXxTZ17HF4cASU1Le+SD70ntrgLh47KAV78ZDs4oiHxt7Bkf8PBntr+kw8XZBcR76VJkIQ358zscp/", - "Pzd+hnYMq3zfr1+xfT0qRHa8IDLRj41jbi+hRpRcLX6CxuIsjWYI2LwKuDgXkGU0rvQjxQttpN+sV7DK", - "l8NIOh5hrJ3kPs+13GIwpdlPkTZy5/v6IYdVfnG9mIO03q2hM1TBpwbdfMZTtgmevPL9U+8LcZlgMIBx", - "hEGqhJaufZXWYjK2NQpdTG5zsk6trpGqwov/Ta5EJtpgxVRURE2cXl52XTeRaXviw/pyfzde3s7zj38s", - "P/KdCT2lRjholfu69m7fs5naKqUmC/vq5wUPPaMQ3q3y5XuRiS2G2At3NWEmz1maLbIxYio+TK4SuUZS", - "FcXUtdY+/xMAAP//TXg/jIwJAAA=", + "H4sIAAAAAAAC/5xWUW/bOAz+K4SeNsBNh9s95W3nbEDQ9hYsa/ZwGwJZomM1suST6KS5of/9QDluksa5", + "2/YSxJL48SP5idR3oXzdeIeOohh/F1FVWMv0911LlQ/mH0nGuwmSNData4wqmIZXxVjceY0WyIPyboM7", + "oApBd4dBFr6ltJIH1OjISBu7b2vQEWylo8jGviBp3Ehkogm+wUAGk6/Sh1rSudc5BeNWELAJGBnYrUBC", + "dxqMg21lVPXCM5gIAf9uMRJqdlogmBhb1CNYSGs0bKRtMYLG0jjUUOzg43SS/77IQQaEhy0tN2r5EL27", + "shqk02B1s9yoEUwZJoCSDgKWbcTkWh4nsHcNpkybK7NBB+rAjnYNJtB9FMrXhWfOzhPEtml8INScIj4p", + "xiKmHIinTFivko+B8rxzIEOQO/AldAZcAEkgrfXbCBJUVwryEBtUpuxK2EOyHX8HjL4NCiFi2GB4FV93", + "CJx43j8RC8zTIcasjSOQrTboVEKhYBTnXyqFkWu/Rhc5KkNYpwDOwtsvpDgO3xckkYLTSBhq4zAOFKJX", + "J8OM4O5+/pmVEDHl4KvwDTqjl4fKfBVckl4KgwXghXiRUlcAjc5Tn65kcMhtL8qDWH8mI0+ZYAgTUIvx", + "X91mz+lbJsiQ5dOD9/kZyxcPqIjBJ9PJHVLl9XlAk+kE6rTXc+eV7iq1EZN2IZqVM27FEaBra6bkQyEy", + "sUX+XeMusXoZ083dPPeuNKtLPYaxb+7m3GhKs2pDiuO8ZehiFrA0j+cw3Toz15JkIeOedLFLcrewruNg", + "eXXxeVByvPpLcPefbs/R7j/dcip/EgydbrxxAz2Sc9XvDppGVAHp1qv1De5mkqqBlEmqUmtIR5nK+gd5", + "0X9mbF3HDocHR0BJXeuL5EOnqTXu4rGCkrNnDcltHNDQ0D040v9BYC9Fn4nHK5KryFZpIgTx7SkTi/zD", + "pfHTt2NY5Pt+fcL2dFSI7HhBZKIbG8fcnl0NZHIx+wEas4s0mt5hc+JwdtnhF2kt0tQZMpJQf7B+O5Ek", + "2f/pVVNWmnp5rMAzrEMbXRLWjZWES6MHjzbBl8b+7/YGQ0zhD5yJyu8b8S/2ziMO5x6f8bOXkV+M8yjj", + "l7J61oGZk3GlH7g9oY30h/UKFvm8fxMcvyFYvJIHLV+mDQZTmv0YbyOPni9vc1jkV+9mU5DWuxVsDVXw", + "sUE3nfAzpwmevPJdr+1uwnWCwQDGEQapEloy6wJivVmj0MV03Z2s06xppKrw6rfRG5GJNlgxFhVRE8fX", + "19vtdiTT9siH1fXeNl7fTvP3f87fs82IHtMk6lOX+7r2bj80mdoihSYLe/K+41eHUQivFvn8tcjEs1LE", + "mxEzecrScJeNEWPxdvQmkWskVVGMXWvt078BAAD//zUAIu4NCwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index 48085009c..af32c3e2d 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -473,7 +473,7 @@ func (c *Controller) PushAuthorizationDetails(ctx echo.Context) error { return err } - ad, err := common.ValidateAuthorizationDetails(&body.AuthorizationDetails) + ad, err := util.ValidateAuthorizationDetails(&body.AuthorizationDetails) if err != nil { return err } @@ -509,7 +509,7 @@ func (c *Controller) prepareClaimDataAuthorizationRequest( ctx context.Context, body *PrepareClaimDataAuthorizationRequest, ) (*PrepareClaimDataAuthorizationResponse, error) { - ad, err := common.ValidateAuthorizationDetails(body.AuthorizationDetails) + ad, err := util.ValidateAuthorizationDetails(body.AuthorizationDetails) if err != nil { return nil, err } @@ -531,27 +531,27 @@ func (c *Controller) prepareClaimDataAuthorizationRequest( return nil, resterr.NewSystemError("OIDC4CIService", "PrepareClaimDataAuthorizationRequest", err) } - var walletInitiatedFlowParams *WalletInitiatedFlowParameters - if resp.WalletInitiatedFlow != nil { - walletInitiatedFlowParams = &WalletInitiatedFlowParameters{ - ProfileID: resp.WalletInitiatedFlow.ProfileID, - ProfileVersion: resp.WalletInitiatedFlow.ProfileVersion, - ClaimEndpoint: resp.WalletInitiatedFlow.ClaimEndpoint, - CredentialTemplateID: resp.WalletInitiatedFlow.CredentialTemplateID, - } - } + //var walletInitiatedFlowParams *WalletInitiatedFlowParameters + //if resp.WalletInitiatedFlow != nil { + // walletInitiatedFlowParams = &WalletInitiatedFlowParameters{ + // ProfileID: resp.WalletInitiatedFlow.ProfileID, + // ProfileVersion: resp.WalletInitiatedFlow.ProfileVersion, + // ClaimEndpoint: resp.WalletInitiatedFlow.ClaimEndpoint, + // CredentialTemplateID: resp.WalletInitiatedFlow.CredentialTemplateID, + // } + //} return &PrepareClaimDataAuthorizationResponse{ - WalletInitiatedFlow: walletInitiatedFlowParams, + WalletInitiatedFlow: resp.WalletInitiatedFlow, AuthorizationRequest: OAuthParameters{ ClientId: profile.OIDCConfig.ClientID, ClientSecret: profile.OIDCConfig.ClientSecretHandle, Scope: resp.Scope, - ResponseType: resp.ResponseType, // empty if resp.WalletInitiatedFlow + ResponseType: resp.ResponseType, }, AuthorizationEndpoint: resp.AuthorizationEndpoint, - PushedAuthorizationRequestEndpoint: lo.ToPtr(resp.PushedAuthorizationRequestEndpoint), // empty if resp.WalletInitiatedFlow - TxId: string(resp.TxID), // empty if resp.WalletInitiatedFlow + PushedAuthorizationRequestEndpoint: lo.ToPtr(resp.PushedAuthorizationRequestEndpoint), + TxId: string(resp.TxID), }, nil } @@ -598,7 +598,8 @@ func (c *Controller) StoreAuthorizationCodeRequest(ctx echo.Context) error { return err } - return util.WriteOutput(ctx)(c.oidc4ciService.StoreAuthorizationCode(ctx.Request().Context(), body.OpState, body.Code)) + return util.WriteOutput(ctx)(c.oidc4ciService.StoreAuthorizationCode(ctx.Request().Context(), + body.OpState, body.Code, body.WalletInitiatedFlow)) } // ExchangeAuthorizationCodeRequest Exchanges authorization code. diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index 4217c3ca6..f8f8125e1 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -177,10 +177,8 @@ type PrepareClaimDataAuthorizationResponse struct { PushedAuthorizationRequestEndpoint *string `json:"pushed_authorization_request_endpoint,omitempty"` // Transaction ID to correlate upcoming authorization response. - TxId string `json:"tx_id"` - - // If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data. - WalletInitiatedFlow *WalletInitiatedFlowParameters `json:"wallet_initiated_flow,omitempty"` + TxId string `json:"tx_id"` + WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow,omitempty"` } // Model for Prepare Credential request. @@ -224,8 +222,9 @@ type PushAuthorizationDetailsRequest struct { // Model for storing auth code from issuer oauth type StoreAuthorizationCodeRequest struct { - Code string `json:"code"` - OpState string `json:"op_state"` + Code string `json:"code"` + OpState string `json:"op_state"` + WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow,omitempty"` } // Response model for storing auth code from issuer oauth @@ -267,14 +266,6 @@ type ValidatePreAuthorizedCodeResponse struct { TxId string `json:"tx_id"` } -// If transaction was initiated by Wallet - object will contain initiate issuance profile-specific data. -type WalletInitiatedFlowParameters struct { - ClaimEndpoint string `json:"claimEndpoint"` - CredentialTemplateID string `json:"credentialTemplateID"` - ProfileID string `json:"profileID"` - ProfileVersion string `json:"profileVersion"` -} - // OpenID Config response. type WellKnownOpenIDConfiguration struct { // URL of the OP's OAuth 2.0 Authorization Endpoint. @@ -339,9 +330,6 @@ type PostIssueCredentialsJSONBody = IssueCredentialData // InitiateCredentialIssuanceJSONBody defines parameters for InitiateCredentialIssuance. type InitiateCredentialIssuanceJSONBody = InitiateOIDC4CIRequest -// InitiateCredentialIssuanceInternalJSONBody defines parameters for InitiateCredentialIssuanceInternal. -type InitiateCredentialIssuanceInternalJSONBody = InitiateOIDC4CIRequest - // PostCredentialsStatusJSONRequestBody defines body for PostCredentialsStatus for application/json ContentType. type PostCredentialsStatusJSONRequestBody = PostCredentialsStatusJSONBody @@ -369,9 +357,6 @@ type PostIssueCredentialsJSONRequestBody = PostIssueCredentialsJSONBody // InitiateCredentialIssuanceJSONRequestBody defines body for InitiateCredentialIssuance for application/json ContentType. type InitiateCredentialIssuanceJSONRequestBody = InitiateCredentialIssuanceJSONBody -// InitiateCredentialIssuanceInternalJSONRequestBody defines body for InitiateCredentialIssuanceInternal for application/json ContentType. -type InitiateCredentialIssuanceInternalJSONRequestBody = InitiateCredentialIssuanceInternalJSONBody - // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -493,11 +478,6 @@ type ClientInterface interface { InitiateCredentialIssuance(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // InitiateCredentialIssuanceInternal request with any body - InitiateCredentialIssuanceInternalWithBody(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) - - InitiateCredentialIssuanceInternal(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) - // OpenidConfig request OpenidConfig(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -733,30 +713,6 @@ func (c *Client) InitiateCredentialIssuance(ctx context.Context, profileID strin return c.Client.Do(req) } -func (c *Client) InitiateCredentialIssuanceInternalWithBody(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewInitiateCredentialIssuanceInternalRequestWithBody(c.Server, profileID, profileVersion, contentType, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - -func (c *Client) InitiateCredentialIssuanceInternal(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewInitiateCredentialIssuanceInternalRequest(c.Server, profileID, profileVersion, body) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if err := c.applyEditors(ctx, req, reqEditors); err != nil { - return nil, err - } - return c.Client.Do(req) -} - func (c *Client) OpenidConfig(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewOpenidConfigRequest(c.Server, profileID, profileVersion) if err != nil { @@ -1210,60 +1166,6 @@ func NewInitiateCredentialIssuanceRequestWithBody(server string, profileID strin return req, nil } -// NewInitiateCredentialIssuanceInternalRequest calls the generic InitiateCredentialIssuanceInternal builder with application/json body -func NewInitiateCredentialIssuanceInternalRequest(server string, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewInitiateCredentialIssuanceInternalRequestWithBody(server, profileID, profileVersion, "application/json", bodyReader) -} - -// NewInitiateCredentialIssuanceInternalRequestWithBody generates requests for InitiateCredentialIssuanceInternal with any type of body -func NewInitiateCredentialIssuanceInternalRequestWithBody(server string, profileID string, profileVersion string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "profileID", runtime.ParamLocationPath, profileID) - if err != nil { - return nil, err - } - - var pathParam1 string - - pathParam1, err = runtime.StyleParamWithLocation("simple", false, "profileVersion", runtime.ParamLocationPath, profileVersion) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/issuer/profiles/%s/%s/interactions/initiate-oidc-internal", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - // NewOpenidConfigRequest generates requests for OpenidConfig func NewOpenidConfigRequest(server string, profileID string, profileVersion string) (*http.Request, error) { var err error @@ -1437,11 +1339,6 @@ type ClientWithResponsesInterface interface { InitiateCredentialIssuanceWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceResponse, error) - // InitiateCredentialIssuanceInternal request with any body - InitiateCredentialIssuanceInternalWithBodyWithResponse(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) - - InitiateCredentialIssuanceInternalWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) - // OpenidConfig request OpenidConfigWithResponse(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*OpenidConfigResponse, error) @@ -1668,28 +1565,6 @@ func (r InitiateCredentialIssuanceResponse) StatusCode() int { return 0 } -type InitiateCredentialIssuanceInternalResponse struct { - Body []byte - HTTPResponse *http.Response - JSON200 *InitiateOIDC4CIResponse -} - -// Status returns HTTPResponse.Status -func (r InitiateCredentialIssuanceInternalResponse) Status() string { - if r.HTTPResponse != nil { - return r.HTTPResponse.Status - } - return http.StatusText(0) -} - -// StatusCode returns HTTPResponse.StatusCode -func (r InitiateCredentialIssuanceInternalResponse) StatusCode() int { - if r.HTTPResponse != nil { - return r.HTTPResponse.StatusCode - } - return 0 -} - type OpenidConfigResponse struct { Body []byte HTTPResponse *http.Response @@ -1896,23 +1771,6 @@ func (c *ClientWithResponses) InitiateCredentialIssuanceWithResponse(ctx context return ParseInitiateCredentialIssuanceResponse(rsp) } -// InitiateCredentialIssuanceInternalWithBodyWithResponse request with arbitrary body returning *InitiateCredentialIssuanceInternalResponse -func (c *ClientWithResponses) InitiateCredentialIssuanceInternalWithBodyWithResponse(ctx context.Context, profileID string, profileVersion string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) { - rsp, err := c.InitiateCredentialIssuanceInternalWithBody(ctx, profileID, profileVersion, contentType, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseInitiateCredentialIssuanceInternalResponse(rsp) -} - -func (c *ClientWithResponses) InitiateCredentialIssuanceInternalWithResponse(ctx context.Context, profileID string, profileVersion string, body InitiateCredentialIssuanceInternalJSONRequestBody, reqEditors ...RequestEditorFn) (*InitiateCredentialIssuanceInternalResponse, error) { - rsp, err := c.InitiateCredentialIssuanceInternal(ctx, profileID, profileVersion, body, reqEditors...) - if err != nil { - return nil, err - } - return ParseInitiateCredentialIssuanceInternalResponse(rsp) -} - // OpenidConfigWithResponse request returning *OpenidConfigResponse func (c *ClientWithResponses) OpenidConfigWithResponse(ctx context.Context, profileID string, profileVersion string, reqEditors ...RequestEditorFn) (*OpenidConfigResponse, error) { rsp, err := c.OpenidConfig(ctx, profileID, profileVersion, reqEditors...) @@ -2181,32 +2039,6 @@ func ParseInitiateCredentialIssuanceResponse(rsp *http.Response) (*InitiateCrede return response, nil } -// ParseInitiateCredentialIssuanceInternalResponse parses an HTTP response from a InitiateCredentialIssuanceInternalWithResponse call -func ParseInitiateCredentialIssuanceInternalResponse(rsp *http.Response) (*InitiateCredentialIssuanceInternalResponse, error) { - bodyBytes, err := ioutil.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } - - response := &InitiateCredentialIssuanceInternalResponse{ - Body: bodyBytes, - HTTPResponse: rsp, - } - - switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest InitiateOIDC4CIResponse - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - } - - return response, nil -} - // ParseOpenidConfigResponse parses an HTTP response from a OpenidConfigWithResponse call func ParseOpenidConfigResponse(rsp *http.Response) (*OpenidConfigResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) @@ -2291,9 +2123,6 @@ type ServerInterface interface { // Initiate OIDC Credential Issuance // (POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc) InitiateCredentialIssuance(ctx echo.Context, profileID string, profileVersion string) error - // Initiate OIDC Credential Issuance - // (POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal) - InitiateCredentialIssuanceInternal(ctx echo.Context, profileID string, profileVersion string) error // Request openid-config // (GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration) OpenidConfig(ctx echo.Context, profileID string, profileVersion string) error @@ -2442,30 +2271,6 @@ func (w *ServerInterfaceWrapper) InitiateCredentialIssuance(ctx echo.Context) er return err } -// InitiateCredentialIssuanceInternal converts echo context to params. -func (w *ServerInterfaceWrapper) InitiateCredentialIssuanceInternal(ctx echo.Context) error { - var err error - // ------------- Path parameter "profileID" ------------- - var profileID string - - err = runtime.BindStyledParameterWithLocation("simple", false, "profileID", runtime.ParamLocationPath, ctx.Param("profileID"), &profileID) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter profileID: %s", err)) - } - - // ------------- Path parameter "profileVersion" ------------- - var profileVersion string - - err = runtime.BindStyledParameterWithLocation("simple", false, "profileVersion", runtime.ParamLocationPath, ctx.Param("profileVersion"), &profileVersion) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter profileVersion: %s", err)) - } - - // Invoke the callback with all the unmarshalled arguments - err = w.Handler.InitiateCredentialIssuanceInternal(ctx, profileID, profileVersion) - return err -} - // OpenidConfig converts echo context to params. func (w *ServerInterfaceWrapper) OpenidConfig(ctx echo.Context) error { var err error @@ -2552,7 +2357,6 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/issuer/interactions/validate-pre-authorized-code", wrapper.ValidatePreAuthorizedCodeRequest) router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/credentials/issue", wrapper.PostIssueCredentials) router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/interactions/initiate-oidc", wrapper.InitiateCredentialIssuance) - router.POST(baseURL+"/issuer/profiles/:profileID/:profileVersion/interactions/initiate-oidc-internal", wrapper.InitiateCredentialIssuanceInternal) router.GET(baseURL+"/issuer/:profileID/:profileVersion/.well-known/openid-configuration", wrapper.OpenidConfig) router.GET(baseURL+"/issuer/:profileID/:profileVersion/.well-known/openid-credential-issuer", wrapper.OpenidCredentialIssuerConfig) diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 18eb2ea46..8abd0caab 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -152,7 +152,7 @@ func (c *Controller) OidcPushedAuthorizationRequest(e echo.Context) error { return resterr.NewValidationError(resterr.InvalidValue, "authorization_details", err) } - authorizationDetails, err := common.ValidateAuthorizationDetails(&ad) + authorizationDetails, err := apiUtil.ValidateAuthorizationDetails(&ad) if err != nil { return err } @@ -219,7 +219,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e return resterr.NewValidationError(resterr.InvalidValue, "authorization_details", err) } - if _, err = common.ValidateAuthorizationDetails(&authorizationDetails); err != nil { + if _, err = apiUtil.ValidateAuthorizationDetails(&authorizationDetails); err != nil { return err } @@ -271,18 +271,6 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e return resterr.NewFositeError(resterr.FositeAuthorizeError, e, c.oauth2Provider, err).WithAuthorizeRequester(ar) } - var walletInitiatedFlowData *oidc4ci.WalletInitiatedFlow - - if claimDataAuth.WalletInitiatedFlow != nil { - walletInitiatedFlowData = &oidc4ci.WalletInitiatedFlow{ - ProfileID: claimDataAuth.WalletInitiatedFlow.ProfileID, - ProfileVersion: claimDataAuth.WalletInitiatedFlow.ProfileVersion, - Scope: claimDataAuth.AuthorizationRequest.Scope, - ClaimEndpoint: claimDataAuth.WalletInitiatedFlow.ClaimEndpoint, - CredentialTemplateID: claimDataAuth.WalletInitiatedFlow.CredentialTemplateID, - } - } - if err = c.stateStore.SaveAuthorizeState( ctx, params.IssuerState, @@ -291,7 +279,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e RespondMode: string(ar.GetResponseMode()), Header: resp.GetHeader(), Parameters: resp.GetParameters(), - WalletInitiatedFlow: walletInitiatedFlowData, + WalletInitiatedFlow: claimDataAuth.WalletInitiatedFlow, }); err != nil { return fmt.Errorf("save authorize state: %w", err) } @@ -396,20 +384,11 @@ func (c *Controller) OidcRedirect(e echo.Context, params OidcRedirectParams) err return apiUtil.WriteOutput(e)(nil, err) } - if resp.WalletInitiatedFlow != nil { - - // todo what auth token I need to use? - //req.Header.Add("Authorization", "Bearer "+token) - if err = c.walletInitiateIssuance(ctx, params.State, resp.WalletInitiatedFlow); err != nil { - return fmt.Errorf("walletInitiateIssuance: %w", err) - } - //todo: to I need to store in cache authorization details and then call c.issuerInteractionClient.PushAuthorizationDetails()? - } - storeResp, storeErr := c.issuerInteractionClient.StoreAuthorizationCodeRequest(ctx, issuer.StoreAuthorizationCodeRequestJSONRequestBody{ - Code: params.Code, - OpState: params.State, + Code: params.Code, + OpState: params.State, + WalletInitiatedFlow: resp.WalletInitiatedFlow, }) if storeErr != nil { return storeErr @@ -437,43 +416,44 @@ func (c *Controller) OidcRedirect(e echo.Context, params OidcRedirectParams) err return nil } -func (c *Controller) walletInitiateIssuance(ctx context.Context, issuerState string, walletInitiatedFlow *oidc4ci.WalletInitiatedFlow) error { - // if wallet initiated flow - need to create transaction here - initiateIssuanceResponse, err := c.issuerInteractionClient.InitiateCredentialIssuanceInternal(ctx, - walletInitiatedFlow.ProfileID, - walletInitiatedFlow.ProfileVersion, - issuer.InitiateCredentialIssuanceJSONRequestBody{ - WalletInitiatedIssuance: lo.ToPtr(true), - ClaimEndpoint: lo.ToPtr(walletInitiatedFlow.ClaimEndpoint), - CredentialTemplateId: lo.ToPtr(walletInitiatedFlow.CredentialTemplateID), - GrantType: lo.ToPtr("authorization_code"), - OpState: lo.ToPtr(issuerState), - ResponseType: lo.ToPtr("code"), - //Scope: lo.ToPtr([]string{"openid", "profile"}), - Scope: lo.ToPtr(walletInitiatedFlow.Scope), - UserPinRequired: lo.ToPtr(false), - - AuthorizationDetails: nil, - ClaimData: nil, // pre-auth flow only - ClientInitiateIssuanceUrl: nil, - ClientWellknown: nil, - CredentialDescription: nil, - CredentialExpiresAt: nil, - CredentialName: nil, - }) - if err != nil { - return err - } - - defer initiateIssuanceResponse.Body.Close() - - if initiateIssuanceResponse.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code %d", initiateIssuanceResponse.StatusCode) - } - - return nil -} +//func (c *Controller) walletInitiateIssuance(ctx context.Context, issuerState string, walletInitiatedFlow *oidc4ci.WalletInitiatedFlow) error { +// // if wallet initiated flow - need to create transaction here +// initiateIssuanceResponse, err := c.issuerInteractionClient.InitiateCredentialIssuanceInternal(ctx, +// walletInitiatedFlow.ProfileID, +// walletInitiatedFlow.ProfileVersion, +// issuer.InitiateCredentialIssuanceJSONRequestBody{ +// WalletInitiatedIssuance: lo.ToPtr(true), +// ClaimEndpoint: lo.ToPtr(walletInitiatedFlow.ClaimEndpoint), +// CredentialTemplateId: lo.ToPtr(walletInitiatedFlow.CredentialTemplateID), +// GrantType: lo.ToPtr("authorization_code"), +// OpState: lo.ToPtr(issuerState), +// ResponseType: lo.ToPtr("code"), +// //Scope: lo.ToPtr([]string{"openid", "profile"}), +// Scope: lo.ToPtr(walletInitiatedFlow.Scope), +// UserPinRequired: lo.ToPtr(false), +// +// AuthorizationDetails: nil, +// ClaimData: nil, // pre-auth flow only +// ClientInitiateIssuanceUrl: nil, +// ClientWellknown: nil, +// CredentialDescription: nil, +// CredentialExpiresAt: nil, +// CredentialName: nil, +// }) +// +// if err != nil { +// return err +// } +// +// defer initiateIssuanceResponse.Body.Close() +// +// if initiateIssuanceResponse.StatusCode != http.StatusOK { +// return fmt.Errorf("unexpected status code %d", initiateIssuanceResponse.StatusCode) +// } +// +// return nil +//} // OidcToken handles OIDC token request (POST /oidc/token). func (c *Controller) OidcToken(e echo.Context) error { @@ -513,7 +493,6 @@ func (c *Controller) OidcToken(e echo.Context) error { txID = resp.TxId } else { - //todo make wallet init specific exchangeResp, errExchange := c.issuerInteractionClient.ExchangeAuthorizationCodeRequest( ctx, issuer.ExchangeAuthorizationCodeRequestJSONRequestBody{ diff --git a/pkg/restapi/v1/util/validate.go b/pkg/restapi/v1/util/validate.go new file mode 100644 index 000000000..a3d005837 --- /dev/null +++ b/pkg/restapi/v1/util/validate.go @@ -0,0 +1,41 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package util + +import ( + "errors" + + "github.com/samber/lo" + + "github.com/trustbloc/vcs/pkg/restapi/resterr" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" + "github.com/trustbloc/vcs/pkg/service/oidc4ci" +) + +func ValidateAuthorizationDetails(ad *common.AuthorizationDetails) (*oidc4ci.AuthorizationDetails, error) { + if ad.Type != "openid_credential" { + return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.type", + errors.New("type should be 'openid_credential'")) + } + + mapped := &oidc4ci.AuthorizationDetails{ + Type: ad.Type, + Types: ad.Types, + Locations: lo.FromPtr(ad.Locations), + } + + if ad.Format != nil { + vcFormat, err := common.ValidateVCFormat(common.VCFormat(*ad.Format)) + if err != nil { + return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.format", err) + } + + mapped.Format = vcFormat + } + + return mapped, nil +} diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index ff696ce68..86487b597 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -16,6 +16,7 @@ import ( "github.com/trustbloc/vcs/pkg/dataprotect" vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" ) // TxID defines type for transaction ID. @@ -137,7 +138,7 @@ type PrepareClaimDataAuthorizationRequest struct { } type PrepareClaimDataAuthorizationResponse struct { - WalletInitiatedFlow *WalletInitiatedFlow + WalletInitiatedFlow *common.WalletInitiatedFlowData ProfileID profileapi.ID ProfileVersion profileapi.Version TxID TxID @@ -170,19 +171,11 @@ type InsertOptions struct { } type AuthorizeState struct { - RedirectURI *url.URL `json:"redirect_uri"` - RespondMode string `json:"respond_mode"` - Header map[string][]string `json:"header"` - Parameters map[string][]string `json:"parameters"` - WalletInitiatedFlow *WalletInitiatedFlow `json:"walletInitiatedFlow,omitempty"` -} - -type WalletInitiatedFlow struct { - ProfileID string `json:"profileId"` - ProfileVersion string `json:"profileVersion"` - Scope []string `json:"scope"` - ClaimEndpoint string `json:"claimEndpoint"` - CredentialTemplateID string `json:"credentialTemplateId"` + RedirectURI *url.URL `json:"redirect_uri"` + RespondMode string `json:"respond_mode"` + Header map[string][]string `json:"header"` + Parameters map[string][]string `json:"parameters"` + WalletInitiatedFlow *common.WalletInitiatedFlowData `json:"wallet_initiated_flow"` } type eventPayload struct { @@ -229,7 +222,7 @@ type ServiceInterface interface { InitiateIssuance(ctx context.Context, req *InitiateIssuanceRequest, profile *profileapi.Issuer) (*InitiateIssuanceResponse, error) //nolint:lll PushAuthorizationDetails(ctx context.Context, opState string, ad *AuthorizationDetails) error PrepareClaimDataAuthorizationRequest(ctx context.Context, req *PrepareClaimDataAuthorizationRequest) (*PrepareClaimDataAuthorizationResponse, error) //nolint:lll - StoreAuthorizationCode(ctx context.Context, opState string, code string) (TxID, error) + StoreAuthorizationCode(ctx context.Context, opState string, code string, flowData *common.WalletInitiatedFlowData) (TxID, error) ExchangeAuthorizationCode(ctx context.Context, opState string) (TxID, error) ValidatePreAuthorizedCodeRequest(ctx context.Context, preAuthorizedCode string, pin string, clientID string) (*Transaction, error) //nolint:lll PrepareCredential(ctx context.Context, req *PrepareCredential) (*PrepareCredentialResult, error) diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 940729210..9797730e5 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -27,6 +27,7 @@ import ( "github.com/trustbloc/vcs/pkg/event/spi" profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" ) const ( @@ -237,7 +238,6 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( } return &PrepareClaimDataAuthorizationResponse{ - WalletInitiatedFlow: nil, ProfileID: tx.ProfileID, ProfileVersion: tx.ProfileVersion, TxID: tx.ID, @@ -336,23 +336,26 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( var credTemplate string // todo Sudesh remove hardcode var credType string - if profile.ID == "bank_issuer" { + + switch profile.ID { // todo Sudesh remove hardcode + case "bank_issuer": credTemplate = "universityDegreeTemplateID" credType = "UniversityDegreeCredential" - } else if profile.ID == "i_myprofile_ud_es256k_jwt" { + case "i_myprofile_ud_es256k_jwt": credTemplate = "permanentResidentCardTemplateID" credType = "PermanentResidentCard" - } else { + default: credTemplate = "crudeProductCredentialTemplateID" credType = "CrudeProductCredential" } return &PrepareClaimDataAuthorizationResponse{ - WalletInitiatedFlow: &WalletInitiatedFlow{ - ProfileID: profileID, - ProfileVersion: profileVersion, - ClaimEndpoint: fmt.Sprintf("https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), // todo Sudesh remove hardcode - CredentialTemplateID: credTemplate, // todo Sudesh remove hardcode + WalletInitiatedFlow: &common.WalletInitiatedFlowData{ + ProfileId: profileID, + ProfileVersion: profileVersion, + ClaimEndpoint: fmt.Sprintf( + "https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), // todo Sudesh remove hardcode + CredentialTemplateId: credTemplate, // todo Sudesh remove hardcode }, ProfileID: profileID, ProfileVersion: profileVersion, diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index 83af8e0c9..65450411e 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/trustbloc/vcs/pkg/event/spi" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" ) // StoreAuthorizationCode stores authorization code from issuer provider. @@ -18,6 +19,7 @@ func (s *Service) StoreAuthorizationCode( ctx context.Context, opState string, code string, + flowData *common.WalletInitiatedFlowData, ) (TxID, error) { tx, err := s.store.FindByOpState(ctx, opState) diff --git a/pkg/service/oidc4ci/util_test.go b/pkg/service/oidc4ci/util_test.go index 80f016c05..682ea3c51 100644 --- a/pkg/service/oidc4ci/util_test.go +++ b/pkg/service/oidc4ci/util_test.go @@ -1,3 +1,9 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + package oidc4ci_test import ( From 3d2c716d3a4696acbfe7ae29671815edcc6aa093 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 14:12:03 +0200 Subject: [PATCH 04/22] feat: more changes Signed-off-by: Stas D --- docs/v1/common.yaml | 3 ++ pkg/restapi/v1/common/openapi.gen.go | 37 +++++++++--------- pkg/restapi/v1/issuer/controller.go | 38 ------------------ pkg/service/oidc4ci/api.go | 1 + pkg/service/oidc4ci/oidc4ci_service.go | 4 +- .../oidc4ci_service_initiate_issuance.go | 1 + .../oidc4ci_service_initiate_issuance_test.go | 1 + .../oidc4ci_service_store_auth_code.go | 39 ++++++++++++++++++- 8 files changed, 66 insertions(+), 58 deletions(-) diff --git a/docs/v1/common.yaml b/docs/v1/common.yaml index 72f914b53..3dde427dc 100644 --- a/docs/v1/common.yaml +++ b/docs/v1/common.yaml @@ -75,6 +75,8 @@ components: type: string profile_version: type: string + op_state: + type: string scopes: type: array items: @@ -89,6 +91,7 @@ components: - scopes - claim_endpoint - credential_template_id + - op_state AuthorizationDetails: title: AuthorizationDetails type: object diff --git a/pkg/restapi/v1/common/openapi.gen.go b/pkg/restapi/v1/common/openapi.gen.go index bace32cf9..1b6a05f11 100644 --- a/pkg/restapi/v1/common/openapi.gen.go +++ b/pkg/restapi/v1/common/openapi.gen.go @@ -94,6 +94,7 @@ type VPFormat string type WalletInitiatedFlowData struct { ClaimEndpoint string `json:"claim_endpoint"` CredentialTemplateId string `json:"credential_template_id"` + OpState string `json:"op_state"` ProfileId string `json:"profile_id"` ProfileVersion string `json:"profile_version"` Scopes []string `json:"scopes"` @@ -102,24 +103,24 @@ type WalletInitiatedFlowData struct { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index af32c3e2d..aff8d0faf 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -397,34 +397,6 @@ func (c *Controller) InitiateCredentialIssuance(e echo.Context, profileID, profi return util.WriteOutput(e)(resp, nil) } -// InitiateCredentialIssuanceInternal initiates OIDC credential issuance flow. -// Only for internal usage. Should bot be exposed to the Internet. -// POST /issuer/profiles/{profileID}/{profileVersion}/interactions/initiate-oidc-internal. -func (c *Controller) InitiateCredentialIssuanceInternal(e echo.Context, profileID, profileVersion string) error { - ctx, span := c.tracer.Start(e.Request().Context(), "InitiateCredentialIssuanceInternal") - defer span.End() - - profile, err := c.accessProfile(profileID, profileVersion) - if err != nil { - return err - } - - var body InitiateOIDC4CIRequest - - if err = util.ReadBody(e, &body); err != nil { - return err - } - - span.SetAttributes(attributeutil.JSON("initiate_issuance_request", body, attributeutil.WithRedacted("claim_data"))) - - resp, err := c.initiateIssuance(ctx, &body, profile) - if err != nil { - return err - } - - return util.WriteOutput(e)(resp, nil) -} - func (c *Controller) initiateIssuance( ctx context.Context, req *InitiateOIDC4CIRequest, @@ -531,16 +503,6 @@ func (c *Controller) prepareClaimDataAuthorizationRequest( return nil, resterr.NewSystemError("OIDC4CIService", "PrepareClaimDataAuthorizationRequest", err) } - //var walletInitiatedFlowParams *WalletInitiatedFlowParameters - //if resp.WalletInitiatedFlow != nil { - // walletInitiatedFlowParams = &WalletInitiatedFlowParameters{ - // ProfileID: resp.WalletInitiatedFlow.ProfileID, - // ProfileVersion: resp.WalletInitiatedFlow.ProfileVersion, - // ClaimEndpoint: resp.WalletInitiatedFlow.ClaimEndpoint, - // CredentialTemplateID: resp.WalletInitiatedFlow.CredentialTemplateID, - // } - //} - return &PrepareClaimDataAuthorizationResponse{ WalletInitiatedFlow: resp.WalletInitiatedFlow, AuthorizationRequest: OAuthParameters{ diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index 86487b597..811943eea 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -127,6 +127,7 @@ type InitiateIssuanceResponse struct { InitiateIssuanceURL string TxID TxID UserPin string + Tx *Transaction `json:"-"` } // PrepareClaimDataAuthorizationRequest is the request to prepare the claim data authorization request. diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 9797730e5..216d24a29 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -184,7 +184,7 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( } // process wallet initiated flow - return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, issuerURL) + return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, issuerURL, req.OpState) } newState := TransactionStateAwaitingIssuerOIDCAuthorization @@ -288,6 +288,7 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( ctx context.Context, requestScopes []string, issuerURL string, + opState string, ) (*PrepareClaimDataAuthorizationResponse, error) { matches := regexp.MustCompile(WalletInitFlowClaimRegex).FindStringSubmatch(issuerURL) if len(matches) != 4 { @@ -356,6 +357,7 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( ClaimEndpoint: fmt.Sprintf( "https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), // todo Sudesh remove hardcode CredentialTemplateId: credTemplate, // todo Sudesh remove hardcode + OpState: opState, }, ProfileID: profileID, ProfileVersion: profileVersion, diff --git a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go index a94806cd7..09ca788cb 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go +++ b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance.go @@ -141,6 +141,7 @@ func (s *Service) InitiateIssuance( // nolint:funlen,gocyclo,gocognit InitiateIssuanceURL: finalURL, TxID: tx.ID, UserPin: tx.UserPin, + Tx: tx, }, nil } diff --git a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go index 8ae81de44..1453326b8 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go @@ -837,6 +837,7 @@ func TestService_InitiateIssuance(t *testing.T) { require.NoError(t, err) resp, err := svc.InitiateIssuance(context.Background(), issuanceReq, profile) + assert.NotNil(t, resp.Tx) tt.check(t, resp, err) }) } diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index 65450411e..240386e36 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -21,7 +21,17 @@ func (s *Service) StoreAuthorizationCode( code string, flowData *common.WalletInitiatedFlowData, ) (TxID, error) { - tx, err := s.store.FindByOpState(ctx, opState) + var tx *Transaction + var err error + if flowData != nil { // its wallet initiated issuance, first we need to initiate issuance + tx, err = s.initiateIssuanceWithWalletFlow(ctx, flowData) + } else { + tx, err = s.store.FindByOpState(ctx, opState) + } + + if err != nil { + return "", err + } if err != nil { return "", fmt.Errorf("get transaction by opstate: %w", err) @@ -40,3 +50,30 @@ func (s *Service) StoreAuthorizationCode( return tx.ID, nil } + +func (s *Service) initiateIssuanceWithWalletFlow( + ctx context.Context, + flowData *common.WalletInitiatedFlowData, +) (*Transaction, error) { + tx, err := s.InitiateIssuance(ctx, &InitiateIssuanceRequest{ + CredentialTemplateID: flowData.CredentialTemplateId, + ClientInitiateIssuanceURL: "", + ClientWellKnownURL: "", + ClaimEndpoint: flowData.ClaimEndpoint, + GrantType: "authorization_code", + ResponseType: "code", + Scope: flowData.Scopes, + OpState: flowData.OpState, + ClaimData: nil, + UserPinRequired: false, + CredentialExpiresAt: nil, + CredentialName: "", + CredentialDescription: "", + WalletInitiatedIssuance: true, + }, nil) + if err != nil { + return nil, fmt.Errorf("can not initiate issuance for wallet flow. %w", err) + } + + return tx.Tx, nil +} From 875f778dcec00d7d87d018b286d4e13ae9b83082 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 14:26:55 +0200 Subject: [PATCH 05/22] chore: nullable Signed-off-by: Stas D --- docs/v1/common.yaml | 2 + docs/v1/openapi.yaml | 1 + pkg/restapi/v1/common/common_test.go | 71 ---------------------- pkg/restapi/v1/common/openapi.gen.go | 36 ++++++------ pkg/restapi/v1/issuer/openapi.gen.go | 4 +- pkg/restapi/v1/util/validate_test.go | 88 ++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 91 deletions(-) create mode 100644 pkg/restapi/v1/util/validate_test.go diff --git a/docs/v1/common.yaml b/docs/v1/common.yaml index 3dde427dc..cee64cdb4 100644 --- a/docs/v1/common.yaml +++ b/docs/v1/common.yaml @@ -70,6 +70,7 @@ components: WalletInitiatedFlowData: title: WalletInitiatedFlowData type: object + nullable: true properties: profile_id: type: string @@ -79,6 +80,7 @@ components: type: string scopes: type: array + nullable: true items: type: string claim_endpoint: diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 3d567bc90..532417308 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -1160,6 +1160,7 @@ components: type: string wallet_initiated_flow: $ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData + nullable: true required: - op_state - code diff --git a/pkg/restapi/v1/common/common_test.go b/pkg/restapi/v1/common/common_test.go index a22088e5f..e37894b3e 100644 --- a/pkg/restapi/v1/common/common_test.go +++ b/pkg/restapi/v1/common/common_test.go @@ -11,14 +11,12 @@ import ( "testing" "github.com/jinzhu/copier" - "github.com/samber/lo" "github.com/stretchr/testify/require" vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" vcskms "github.com/trustbloc/vcs/pkg/kms" profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" - "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) func TestController_MapToKMSConfigType(t *testing.T) { @@ -228,75 +226,6 @@ func TestValidateDIDMethod(t *testing.T) { require.Error(t, err) } -func TestValidateAuthorizationDetails(t *testing.T) { - t.Run("success", func(t *testing.T) { - tests := []struct { - name string - arg *string - want vcsverifiable.Format - }{ - { - name: "ldp_vc format", - arg: lo.ToPtr(string(LdpVc)), - want: vcsverifiable.Ldp, - }, - { - name: "jwt_vc format", - arg: lo.ToPtr(string(JwtVcJsonLd)), - want: vcsverifiable.Jwt, - }, - { - name: "no format", - arg: nil, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ad := &AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: tt.arg, - Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), - } - - got, err := ValidateAuthorizationDetails(ad) - require.NoError(t, err) - require.Equal(t, &oidc4ci.AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: tt.want, - Locations: []string{"https://example.com/rs1", "https://example.com/rs2"}, - }, got) - }) - } - }) - - t.Run("invalid format", func(t *testing.T) { - ad := &AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: lo.ToPtr("invalid"), - } - - got, err := ValidateAuthorizationDetails(ad) - require.ErrorContains(t, err, "unsupported vc format") - require.Nil(t, got) - }) - - t.Run("type should be 'openid_credential'", func(t *testing.T) { - ad := &AuthorizationDetails{ - Type: "invalid", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: lo.ToPtr("ldp_vc"), - } - - got, err := ValidateAuthorizationDetails(ad) - require.ErrorContains(t, err, "type should be 'openid_credential'") - require.Nil(t, got) - }) -} - func strPtr(str string) *string { return &str } diff --git a/pkg/restapi/v1/common/openapi.gen.go b/pkg/restapi/v1/common/openapi.gen.go index 1b6a05f11..80dac2cea 100644 --- a/pkg/restapi/v1/common/openapi.gen.go +++ b/pkg/restapi/v1/common/openapi.gen.go @@ -103,24 +103,24 @@ type WalletInitiatedFlowData struct { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index f8f8125e1..ba9aebb64 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -178,7 +178,7 @@ type PrepareClaimDataAuthorizationResponse struct { // Transaction ID to correlate upcoming authorization response. TxId string `json:"tx_id"` - WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow,omitempty"` + WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow"` } // Model for Prepare Credential request. @@ -224,7 +224,7 @@ type PushAuthorizationDetailsRequest struct { type StoreAuthorizationCodeRequest struct { Code string `json:"code"` OpState string `json:"op_state"` - WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow,omitempty"` + WalletInitiatedFlow *externalRef0.WalletInitiatedFlowData `json:"wallet_initiated_flow"` } // Response model for storing auth code from issuer oauth diff --git a/pkg/restapi/v1/util/validate_test.go b/pkg/restapi/v1/util/validate_test.go new file mode 100644 index 000000000..471dbd799 --- /dev/null +++ b/pkg/restapi/v1/util/validate_test.go @@ -0,0 +1,88 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package util_test + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" + "github.com/trustbloc/vcs/pkg/restapi/v1/util" + "github.com/trustbloc/vcs/pkg/service/oidc4ci" +) + +func TestValidateAuthorizationDetails(t *testing.T) { + t.Run("success", func(t *testing.T) { + tests := []struct { + name string + arg *string + want vcsverifiable.Format + }{ + { + name: "ldp_vc format", + arg: lo.ToPtr(string(common.LdpVc)), + want: vcsverifiable.Ldp, + }, + { + name: "jwt_vc format", + arg: lo.ToPtr(string(common.JwtVcJsonLd)), + want: vcsverifiable.Jwt, + }, + { + name: "no format", + arg: nil, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ad := &common.AuthorizationDetails{ + Type: "openid_credential", + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + Format: tt.arg, + Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), + } + + got, err := util.ValidateAuthorizationDetails(ad) + require.NoError(t, err) + require.Equal(t, &oidc4ci.AuthorizationDetails{ + Type: "openid_credential", + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + Format: tt.want, + Locations: []string{"https://example.com/rs1", "https://example.com/rs2"}, + }, got) + }) + } + }) + + t.Run("invalid format", func(t *testing.T) { + ad := &common.AuthorizationDetails{ + Type: "openid_credential", + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + Format: lo.ToPtr("invalid"), + } + + got, err := util.ValidateAuthorizationDetails(ad) + require.ErrorContains(t, err, "unsupported vc format") + require.Nil(t, got) + }) + + t.Run("type should be 'openid_credential'", func(t *testing.T) { + ad := &common.AuthorizationDetails{ + Type: "invalid", + Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + Format: lo.ToPtr("ldp_vc"), + } + + got, err := util.ValidateAuthorizationDetails(ad) + require.ErrorContains(t, err, "type should be 'openid_credential'") + require.Nil(t, got) + }) +} From f3d41c4a123981c7dc28d39a61974df21444cb9e Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 14:30:08 +0200 Subject: [PATCH 06/22] fix: build Signed-off-by: Stas D --- pkg/restapi/v1/common/openapi.gen.go | 49 ++++++++++--------- .../oidc4ci_service_store_auth_code.go | 4 +- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/pkg/restapi/v1/common/openapi.gen.go b/pkg/restapi/v1/common/openapi.gen.go index 80dac2cea..aab95ad09 100644 --- a/pkg/restapi/v1/common/openapi.gen.go +++ b/pkg/restapi/v1/common/openapi.gen.go @@ -92,35 +92,36 @@ type VPFormat string // WalletInitiatedFlowData defines model for WalletInitiatedFlowData. type WalletInitiatedFlowData struct { - ClaimEndpoint string `json:"claim_endpoint"` - CredentialTemplateId string `json:"credential_template_id"` - OpState string `json:"op_state"` - ProfileId string `json:"profile_id"` - ProfileVersion string `json:"profile_version"` - Scopes []string `json:"scopes"` + ClaimEndpoint string `json:"claim_endpoint"` + CredentialTemplateId string `json:"credential_template_id"` + OpState string `json:"op_state"` + ProfileId string `json:"profile_id"` + ProfileVersion string `json:"profile_version"` + Scopes *[]string `json:"scopes"` } // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index 240386e36..21878427e 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -10,6 +10,8 @@ import ( "context" "fmt" + "github.com/samber/lo" + "github.com/trustbloc/vcs/pkg/event/spi" "github.com/trustbloc/vcs/pkg/restapi/v1/common" ) @@ -62,7 +64,7 @@ func (s *Service) initiateIssuanceWithWalletFlow( ClaimEndpoint: flowData.ClaimEndpoint, GrantType: "authorization_code", ResponseType: "code", - Scope: flowData.Scopes, + Scope: lo.FromPtr(flowData.Scopes), OpState: flowData.OpState, ClaimData: nil, UserPinRequired: false, From 4bae85ee99a845d2a15d60d2be17fd9294c17d63 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 14:45:58 +0200 Subject: [PATCH 07/22] feat: more cleanup Signed-off-by: Stas D --- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 19 ++++++---- .../wrappers/oidc4ci/oidc4ci_wrapper_test.go | 4 +- pkg/restapi/v1/oidc4ci/controller.go | 38 ------------------- pkg/service/oidc4ci/api.go | 31 ++++++++++----- pkg/service/oidc4ci/oidc4ci_service.go | 24 ------------ .../oidc4ci_service_store_auth_code.go | 11 +++--- .../oidc4ci_service_store_auth_code_test.go | 8 ++-- 7 files changed, 45 insertions(+), 90 deletions(-) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index f9a1ec641..bfb94e068 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -69,7 +69,7 @@ func WithClientID(clientID string) OauthClientOpt { func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow") - + ctx := context.Background() log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL) offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl( config.InitiateIssuanceURL, @@ -80,7 +80,7 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { } s.print("Getting issuer OIDC config") - oidcConfig, err := s.getIssuerOIDCConfig(offerResponse.CredentialIssuer) + oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer) if err != nil { return fmt.Errorf("get issuer oidc config: %w", err) } @@ -163,7 +163,7 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { return fmt.Errorf("auth code is empty") } - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, s.httpClient) + ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient) var beforeTokenRequestHooks []OauthClientOpt @@ -246,7 +246,7 @@ func extractIssuerUrlFromScopes(scopes []string) (string, error) { func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow Wallet initiated") - + ctx := context.Background() // Check whether scope contains combined string Issuer URL||ClaimEndpoint||credentialTemplateID issuerUrl, err := extractIssuerUrlFromScopes(config.Scope) if err != nil { @@ -263,7 +263,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) return fmt.Errorf("get issuer oidc issuer config: %w", err) } - oidcConfig, err := s.getIssuerOIDCConfig(issuerUrl) + oidcConfig, err := s.getIssuerOIDCConfig(ctx, issuerUrl) if err != nil { return fmt.Errorf("get issuer oidc config: %w", err) } @@ -339,7 +339,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) return fmt.Errorf("auth code is empty") } - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, s.httpClient) + ctx = context.WithValue(ctx, oauth2.HTTPClient, s.httpClient) var beforeTokenRequestHooks []OauthClientOpt @@ -409,10 +409,15 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) } func (s *Service) getIssuerOIDCConfig( + ctx context.Context, issuerURL string, ) (*issuerv1.WellKnownOpenIDConfiguration, error) { // GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration - resp, err := s.httpClient.Get(issuerURL + "/.well-known/openid-configuration") + req, err := http.NewRequestWithContext(ctx, "GET", issuerURL+"/.well-known/openid-configuration", nil) + if err != nil { + return nil, err + } + resp, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("get issuer well-known: %w", err) } diff --git a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go index 934189cec..d2a89588e 100644 --- a/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go +++ b/pkg/observability/tracing/wrappers/oidc4ci/oidc4ci_wrapper_test.go @@ -65,11 +65,11 @@ func TestWrapper_StoreAuthorizationCode(t *testing.T) { ctrl := gomock.NewController(t) svc := NewMockService(ctrl) - svc.EXPECT().StoreAuthorizationCode(gomock.Any(), "opState", "code").Times(1) + svc.EXPECT().StoreAuthorizationCode(gomock.Any(), "opState", "code", nil).Times(1) w := Wrap(svc, trace.NewNoopTracerProvider().Tracer("")) - _, err := w.StoreAuthorizationCode(context.Background(), "opState", "code") + _, err := w.StoreAuthorizationCode(context.Background(), "opState", "code", nil) require.NoError(t, err) } diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 8abd0caab..4d28d2768 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -417,44 +417,6 @@ func (c *Controller) OidcRedirect(e echo.Context, params OidcRedirectParams) err return nil } -//func (c *Controller) walletInitiateIssuance(ctx context.Context, issuerState string, walletInitiatedFlow *oidc4ci.WalletInitiatedFlow) error { -// // if wallet initiated flow - need to create transaction here -// initiateIssuanceResponse, err := c.issuerInteractionClient.InitiateCredentialIssuanceInternal(ctx, -// walletInitiatedFlow.ProfileID, -// walletInitiatedFlow.ProfileVersion, -// issuer.InitiateCredentialIssuanceJSONRequestBody{ -// WalletInitiatedIssuance: lo.ToPtr(true), -// ClaimEndpoint: lo.ToPtr(walletInitiatedFlow.ClaimEndpoint), -// CredentialTemplateId: lo.ToPtr(walletInitiatedFlow.CredentialTemplateID), -// GrantType: lo.ToPtr("authorization_code"), -// OpState: lo.ToPtr(issuerState), -// ResponseType: lo.ToPtr("code"), -// //Scope: lo.ToPtr([]string{"openid", "profile"}), -// Scope: lo.ToPtr(walletInitiatedFlow.Scope), -// UserPinRequired: lo.ToPtr(false), -// -// AuthorizationDetails: nil, -// ClaimData: nil, // pre-auth flow only -// ClientInitiateIssuanceUrl: nil, -// ClientWellknown: nil, -// CredentialDescription: nil, -// CredentialExpiresAt: nil, -// CredentialName: nil, -// }) -// -// if err != nil { -// return err -// } -// -// defer initiateIssuanceResponse.Body.Close() -// -// if initiateIssuanceResponse.StatusCode != http.StatusOK { -// return fmt.Errorf("unexpected status code %d", initiateIssuanceResponse.StatusCode) -// } -// -// return nil -//} - // OidcToken handles OIDC token request (POST /oidc/token). func (c *Controller) OidcToken(e echo.Context) error { req := e.Request() diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index 811943eea..988c0efee 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -213,18 +213,29 @@ type CredentialOfferResponse struct { Grants CredentialOfferGrant `json:"grants"` } -// wellKnownOpenIDConfiguration OpenID Config response. -type wellKnownOpenIDConfiguration struct { - // JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. - WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` -} - type ServiceInterface interface { - InitiateIssuance(ctx context.Context, req *InitiateIssuanceRequest, profile *profileapi.Issuer) (*InitiateIssuanceResponse, error) //nolint:lll + InitiateIssuance( + ctx context.Context, + req *InitiateIssuanceRequest, + profile *profileapi.Issuer, + ) (*InitiateIssuanceResponse, error) PushAuthorizationDetails(ctx context.Context, opState string, ad *AuthorizationDetails) error - PrepareClaimDataAuthorizationRequest(ctx context.Context, req *PrepareClaimDataAuthorizationRequest) (*PrepareClaimDataAuthorizationResponse, error) //nolint:lll - StoreAuthorizationCode(ctx context.Context, opState string, code string, flowData *common.WalletInitiatedFlowData) (TxID, error) + PrepareClaimDataAuthorizationRequest( + ctx context.Context, + req *PrepareClaimDataAuthorizationRequest, + ) (*PrepareClaimDataAuthorizationResponse, error) + StoreAuthorizationCode( + ctx context.Context, + opState string, + code string, + flowData *common.WalletInitiatedFlowData, + ) (TxID, error) ExchangeAuthorizationCode(ctx context.Context, opState string) (TxID, error) - ValidatePreAuthorizedCodeRequest(ctx context.Context, preAuthorizedCode string, pin string, clientID string) (*Transaction, error) //nolint:lll + ValidatePreAuthorizedCodeRequest( + ctx context.Context, + preAuthorizedCode string, + pin string, + clientID string, + ) (*Transaction, error) PrepareCredential(ctx context.Context, req *PrepareCredential) (*PrepareCredentialResult, error) } diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 216d24a29..af3ce923a 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -248,30 +248,6 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( }, nil } -func (s *Service) getIssuerOIDCConfig( - issuerURL string, -) (*wellKnownOpenIDConfiguration, error) { - // GET /issuer/{profileID}/{profileVersion}/.well-known/openid-configuration - resp, err := s.httpClient.Get(issuerURL + "/.well-known/openid-configuration") - if err != nil { - return nil, fmt.Errorf("get issuer well-known: %w", err) - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("get issuer well-known: status code %d", resp.StatusCode) - } - - var oidcConfig wellKnownOpenIDConfiguration - - if err = json.NewDecoder(resp.Body).Decode(&oidcConfig); err != nil { - return nil, fmt.Errorf("decode issuer well-known: %w", err) - } - - return &oidcConfig, nil -} - func (s *Service) extractIssuerURLFromClaims(requestScopes []string) string { matchRegex := regexp.MustCompile(WalletInitFlowClaimRegex) diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index 21878427e..fa348e77a 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -35,10 +35,6 @@ func (s *Service) StoreAuthorizationCode( return "", err } - if err != nil { - return "", fmt.Errorf("get transaction by opstate: %w", err) - } - tx.IssuerAuthCode = code if err = s.store.Update(ctx, tx); err != nil { s.sendFailedTransactionEvent(ctx, tx, err) @@ -57,6 +53,11 @@ func (s *Service) initiateIssuanceWithWalletFlow( ctx context.Context, flowData *common.WalletInitiatedFlowData, ) (*Transaction, error) { + profile, err := s.profileService.GetProfile(flowData.ProfileId, flowData.ProfileVersion) + if err != nil { + return nil, err + } + tx, err := s.InitiateIssuance(ctx, &InitiateIssuanceRequest{ CredentialTemplateID: flowData.CredentialTemplateId, ClientInitiateIssuanceURL: "", @@ -72,7 +73,7 @@ func (s *Service) initiateIssuanceWithWalletFlow( CredentialName: "", CredentialDescription: "", WalletInitiatedIssuance: true, - }, nil) + }, profile) if err != nil { return nil, fmt.Errorf("can not initiate issuance for wallet flow. %w", err) } diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go index 271290a83..be0be17b2 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go @@ -35,7 +35,7 @@ func TestStoreAuthCode(t *testing.T) { store.EXPECT().FindByOpState(gomock.Any(), opState). Return(nil, errors.New("not found")) - resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, "1234") + resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, "1234", nil) assert.Empty(t, resp) assert.ErrorContains(t, storeErr, "not found") }) @@ -66,7 +66,7 @@ func TestStoreAuthCode(t *testing.T) { return nil }) - resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code) + resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code, nil) assert.ErrorContains(t, storeErr, "update error") assert.NotEqual(t, tx.ID, resp) }) @@ -97,7 +97,7 @@ func TestStoreAuthCode(t *testing.T) { return nil }) - resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code) + resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code, nil) assert.NoError(t, storeErr) assert.Equal(t, tx.ID, resp) }) @@ -136,7 +136,7 @@ func TestStoreAuthCode(t *testing.T) { return nil }) - resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code) + resp, storeErr := srv.StoreAuthorizationCode(context.TODO(), opState, code, nil) assert.ErrorContains(t, storeErr, "publish error") assert.NotEqual(t, tx.ID, resp) }) From d0ff573d4e12482bb17950e64a27c1edca6fa5ed Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 14:58:33 +0200 Subject: [PATCH 08/22] fix: more lint Signed-off-by: Stas D --- .../wallet_runner_oidc4ci_pre_auth.go | 4 +++- pkg/restapi/v1/issuer/controller_test.go | 23 ++++++++++++++++++- pkg/service/oidc4ci/oidc4ci_service.go | 7 +++--- pkg/service/oidc4ci/regex.go | 3 ++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go index 96fdf7275..565b009e7 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go @@ -8,6 +8,7 @@ package walletrunner import ( "bufio" + "context" "encoding/json" "fmt" "io" @@ -29,6 +30,7 @@ import ( func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credential, error) { log.Println("Starting OIDC4VCI pre-authorized code flow") + ctx := context.Background() log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL) offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(config.InitiateIssuanceURL, s.httpClient) if err != nil { @@ -37,7 +39,7 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti s.print("Getting issuer OIDC config") startTime := time.Now() - oidcConfig, err := s.getIssuerOIDCConfig(offerResponse.CredentialIssuer) + oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer) s.perfInfo.VcsCIFlowDuration += time.Since(startTime) // oidc config s.perfInfo.GetIssuerOIDCConfig = time.Since(startTime) diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index 07305dbc9..24ab75bb7 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -35,6 +35,7 @@ import ( "github.com/trustbloc/vcs/pkg/kms/mocks" profileapi "github.com/trustbloc/vcs/pkg/profile" "github.com/trustbloc/vcs/pkg/restapi/resterr" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" "github.com/trustbloc/vcs/pkg/restapi/v1/util" "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) @@ -1120,7 +1121,7 @@ func TestController_StoreAuthZCode(t *testing.T) { opState := uuid.NewString() code := uuid.NewString() mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) - mockOIDC4CIService.EXPECT().StoreAuthorizationCode(gomock.Any(), opState, code).Return(oidc4ci.TxID("1234"), nil) + mockOIDC4CIService.EXPECT().StoreAuthorizationCode(gomock.Any(), opState, code, nil).Return(oidc4ci.TxID("1234"), nil) c := &Controller{ oidc4ciService: mockOIDC4CIService, @@ -1130,6 +1131,26 @@ func TestController_StoreAuthZCode(t *testing.T) { ctx := echoContext(withRequestBody([]byte(req))) assert.NoError(t, c.StoreAuthorizationCodeRequest(ctx)) }) + + t.Run("success with flow data", func(t *testing.T) { + opState := uuid.NewString() + code := uuid.NewString() + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().StoreAuthorizationCode(gomock.Any(), opState, code, + &common.WalletInitiatedFlowData{ + ProfileId: "123", + ProfileVersion: "xxx", + }). + Return(oidc4ci.TxID("1234"), nil) + + c := &Controller{ + oidc4ciService: mockOIDC4CIService, + } + + req := fmt.Sprintf(`{"op_state":"%s","code":"%s", "wallet_initiated_flow" : {"profile_id" : "123", "profile_version": "xxx"}}`, opState, code) //nolint:lll + ctx := echoContext(withRequestBody([]byte(req))) + assert.NoError(t, c.StoreAuthorizationCodeRequest(ctx)) + }) t.Run("invalid body", func(t *testing.T) { c := &Controller{} diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index af3ce923a..e1383245b 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -267,10 +267,11 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( opState string, ) (*PrepareClaimDataAuthorizationResponse, error) { matches := regexp.MustCompile(WalletInitFlowClaimRegex).FindStringSubmatch(issuerURL) - if len(matches) != 4 { + if len(matches) != WalletInitFlowClaimExpectedMatchCount { logger.Error("invalid issuer url for wallet initiated flow", log.WithURL(issuerURL)) return nil, errors.New("invalid issuer url") } + profileID, profileVersion := matches[2], matches[3] profile, err := s.profileService.GetProfile(profileID, profileVersion) @@ -330,8 +331,8 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( WalletInitiatedFlow: &common.WalletInitiatedFlowData{ ProfileId: profileID, ProfileVersion: profileVersion, - ClaimEndpoint: fmt.Sprintf( - "https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), // todo Sudesh remove hardcode + ClaimEndpoint: fmt.Sprintf( // todo Sudesh remove hardcode + "https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), CredentialTemplateId: credTemplate, // todo Sudesh remove hardcode OpState: opState, }, diff --git a/pkg/service/oidc4ci/regex.go b/pkg/service/oidc4ci/regex.go index 23b26dd5d..2bf68de85 100644 --- a/pkg/service/oidc4ci/regex.go +++ b/pkg/service/oidc4ci/regex.go @@ -1,5 +1,6 @@ package oidc4ci const ( - WalletInitFlowClaimRegex = `(https|http):\/\/.*\/issuer\/(.*?)\/(.*)$` + WalletInitFlowClaimRegex = `(https|http):\/\/.*\/issuer\/(.*?)\/(.*)$` + WalletInitFlowClaimExpectedMatchCount = 4 ) From 7b2888f57a3817cd134e7536a94ffa8831508041 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 15:11:42 +0200 Subject: [PATCH 09/22] fix: unit Signed-off-by: Stas D --- pkg/service/oidc4ci/oidc4ci_service.go | 58 +++++++++++-------- .../oidc4ci_service_initiate_issuance_test.go | 4 +- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index e1383245b..c5b919381 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -165,17 +165,39 @@ func (s *Service) PushAuthorizationDetails( return nil } +func (s *Service) checkScopes(reqScopes []string, txScopes []string) error { + isScopeValid := true + + for _, scope := range reqScopes { + found := false + + for _, v := range txScopes { + if v == scope { + found = true + break + } + } + + if !found { + isScopeValid = false + break + } + } + + if !isScopeValid { + return ErrInvalidScope + } + + return nil +} + func (s *Service) PrepareClaimDataAuthorizationRequest( ctx context.Context, req *PrepareClaimDataAuthorizationRequest, ) (*PrepareClaimDataAuthorizationResponse, error) { tx, err := s.store.FindByOpState(ctx, req.OpState) - if err != nil { - if !errors.Is(err, ErrDataNotFound) { - return nil, err - } - + if err != nil && errors.Is(err, ErrDataNotFound) { // check if issuer supports Wallet initiated flow issuerURL := s.extractIssuerURLFromClaims(req.Scope) if issuerURL == "" { @@ -187,6 +209,10 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, issuerURL, req.OpState) } + if err != nil { + return nil, err + } + newState := TransactionStateAwaitingIssuerOIDCAuthorization if err = s.validateStateTransition(tx.State, newState); err != nil { s.sendFailedTransactionEvent(ctx, tx, err) @@ -198,26 +224,8 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( return nil, ErrResponseTypeMismatch } - isScopeValid := true - - for _, scope := range req.Scope { - found := false - - for _, v := range tx.Scope { - if v == scope { - found = true - break - } - } - - if !found { - isScopeValid = false - break - } - } - - if !isScopeValid { - return nil, ErrInvalidScope + if err = s.checkScopes(req.Scope, tx.Scope); err != nil { + return nil, err } if req.AuthorizationDetails != nil { diff --git a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go index 1453326b8..39f476ee2 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go @@ -111,6 +111,7 @@ func TestService_InitiateIssuance(t *testing.T) { }, check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) { require.NoError(t, err) + assert.NotNil(t, resp.Tx) require.Contains(t, resp.InitiateIssuanceURL, "https://wallet.example.com/initiate_issuance") }, }, @@ -197,6 +198,7 @@ func TestService_InitiateIssuance(t *testing.T) { check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) { require.NoError(t, err) assert.Equal(t, "123456789", resp.UserPin) + assert.NotNil(t, resp.Tx) require.Equal(t, "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fvcs.pb.example.com%2Fissuer%2Ftest_issuer%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json-ld%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22super-secret-pre-auth-code%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D", //nolint resp.InitiateIssuanceURL) @@ -276,6 +278,7 @@ func TestService_InitiateIssuance(t *testing.T) { }, check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) { require.NoError(t, err) + assert.NotNil(t, resp.Tx) require.Equal(t, "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fvcs.pb.example.com%2Fissuer%2Ftest_issuer%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json-ld%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22super-secret-pre-auth-code%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D", //nolint resp.InitiateIssuanceURL) }, @@ -837,7 +840,6 @@ func TestService_InitiateIssuance(t *testing.T) { require.NoError(t, err) resp, err := svc.InitiateIssuance(context.Background(), issuanceReq, profile) - assert.NotNil(t, resp.Tx) tt.check(t, resp, err) }) } From ebe1c8d156a9ab236761e645033c91ccb1292c24 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 16:20:32 +0200 Subject: [PATCH 10/22] fix: test Signed-off-by: Stas D --- pkg/service/oidc4ci/oidc4ci_service_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index 4f86632bd..6988f3423 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -353,7 +353,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { } }, check: func(t *testing.T, resp *oidc4ci.PrepareClaimDataAuthorizationResponse, err error) { - require.ErrorContains(t, err, "find tx by op state") + require.ErrorContains(t, err, "find tx error") require.Nil(t, resp) }, }, From e2c0c9b00e120279a27c6757445ed911cf36a112 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Mon, 17 Jul 2023 16:46:51 +0200 Subject: [PATCH 11/22] feat: more coverage Signed-off-by: Stas D --- .../oidc4ci_service_initiate_issuance_test.go | 58 +++++++++++++++++++ .../oidc4cinoncestore/oidc4vc_store_test.go | 19 +++--- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go index 39f476ee2..47affa430 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_initiate_issuance_test.go @@ -115,6 +115,64 @@ func TestService_InitiateIssuance(t *testing.T) { require.Contains(t, resp.InitiateIssuanceURL, "https://wallet.example.com/initiate_issuance") }, }, + { + name: "Success wallet flow", + setup: func() { + mockTransactionStore.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func( + ctx context.Context, + data *oidc4ci.TransactionData, + params ...func(insertOptions *oidc4ci.InsertOptions), + ) (*oidc4ci.Transaction, error) { + assert.Equal(t, oidc4ci.TransactionStateAwaitingIssuerOIDCAuthorization, data.State) + + return &oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + CredentialFormat: verifiable.Jwt, + State: data.State, + CredentialTemplate: &profileapi.CredentialTemplate{ + ID: "templateID", + }, + }, + }, nil + }) + + mockWellKnownService.EXPECT().GetOIDCConfiguration(gomock.Any(), issuerWellKnownURL).Return( + &oidc4ci.OIDCConfiguration{}, nil) + + mockWellKnownService.EXPECT().GetOIDCConfiguration(gomock.Any(), walletWellKnownURL).Return( + &oidc4ci.OIDCConfiguration{ + InitiateIssuanceEndpoint: "https://wallet.example.com/initiate_issuance", + }, nil) + + eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, messages ...*spi.Event) error { + assert.Len(t, messages, 1) + assert.Equal(t, messages[0].Type, spi.IssuerOIDCInteractionInitiated) + + return nil + }) + + issuanceReq = &oidc4ci.InitiateIssuanceRequest{ + CredentialTemplateID: "templateID", + ClientWellKnownURL: walletWellKnownURL, + ClaimEndpoint: "https://vcs.pb.example.com/claim", + OpState: "eyJhbGciOiJSU0Et", + GrantType: "authorization_code", + Scope: []string{"openid", "profile"}, + WalletInitiatedIssuance: true, + } + + profile = &testProfile + }, + check: func(t *testing.T, resp *oidc4ci.InitiateIssuanceResponse, err error) { + require.NoError(t, err) + assert.NotNil(t, resp.Tx) + assert.Equal(t, oidc4ci.TransactionStateAwaitingIssuerOIDCAuthorization, resp.Tx.State) + require.Contains(t, resp.InitiateIssuanceURL, "https://wallet.example.com/initiate_issuance") + }, + }, { name: "Success Pre-Auth with PIN", setup: func() { diff --git a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go index f21bbe42b..be8269fd0 100644 --- a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go +++ b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go @@ -111,15 +111,16 @@ func TestStore(t *testing.T) { Format: "vxcxzcz", Locations: []string{"loc1", "loc2"}, }, - IssuerAuthCode: uuid.NewString(), - IssuerToken: uuid.NewString(), - OpState: id, - UserPin: "321", - IsPreAuthFlow: true, - PreAuthCode: uuid.NewString(), - WebHookURL: "http://remote-url", - DID: "did:123", - ClaimDataID: uuid.NewString(), + IssuerAuthCode: uuid.NewString(), + IssuerToken: uuid.NewString(), + OpState: id, + UserPin: "321", + IsPreAuthFlow: true, + PreAuthCode: uuid.NewString(), + WebHookURL: "http://remote-url", + DID: "did:123", + ClaimDataID: uuid.NewString(), + WalletInitiatedIssuance: true, } var resp *oidc4ci.Transaction From cb7c4210a53eef90d0fabf01cf85f33399d29bbd Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 15:29:30 +0200 Subject: [PATCH 12/22] feat: improve codecov Signed-off-by: Stas D --- pkg/profile/api.go | 1 + pkg/service/oidc4ci/errors.go | 1 + pkg/service/oidc4ci/oidc4ci_service.go | 53 ++- .../oidc4ci_service_store_auth_code_test.go | 149 ++++++++ pkg/service/oidc4ci/oidc4ci_service_test.go | 352 ++++++++++++++++++ test/bdd/fixtures/profile/profiles.json | 55 +-- 6 files changed, 555 insertions(+), 56 deletions(-) diff --git a/pkg/profile/api.go b/pkg/profile/api.go index c1be98bf6..13ba85c63 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -81,6 +81,7 @@ type OIDCConfig struct { InitialAccessTokenLifespan time.Duration `json:"initial_access_token_lifespan"` PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported"` WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` + ClaimsEndpoint string `json:"claims_endpoint"` } // VCConfig describes how to sign verifiable credentials. diff --git a/pkg/service/oidc4ci/errors.go b/pkg/service/oidc4ci/errors.go index 81f41d7fe..18a3c23e2 100644 --- a/pkg/service/oidc4ci/errors.go +++ b/pkg/service/oidc4ci/errors.go @@ -21,4 +21,5 @@ var ( ErrCredentialTypeNotSupported = errors.New("credential type not supported") ErrCredentialFormatNotSupported = errors.New("credential format not supported") ErrVCOptionsNotConfigured = errors.New("vc options not configured") + ErrInvalidIssuerURL = errors.New("invalid issuer url") ) diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index c5b919381..7f50053a1 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -198,15 +198,18 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( tx, err := s.store.FindByOpState(ctx, req.OpState) if err != nil && errors.Is(err, ErrDataNotFound) { - // check if issuer supports Wallet initiated flow - issuerURL := s.extractIssuerURLFromClaims(req.Scope) - if issuerURL == "" { - // otherwise - return regular error + // process wallet initiated flow + walletFlowResp, walletFlowErr := s.prepareClaimDataAuthorizationRequestWalletInitiated( + ctx, + req.Scope, + s.extractIssuerURLFromClaims(req.Scope), + req.OpState, + ) + if walletFlowErr != nil && errors.Is(walletFlowErr, ErrInvalidIssuerURL) { // not wallet flow return nil, err } - // process wallet initiated flow - return s.prepareClaimDataAuthorizationRequestWalletInitiated(ctx, req.Scope, issuerURL, req.OpState) + return walletFlowResp, walletFlowErr } if err != nil { @@ -277,7 +280,7 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( matches := regexp.MustCompile(WalletInitFlowClaimRegex).FindStringSubmatch(issuerURL) if len(matches) != WalletInitFlowClaimExpectedMatchCount { logger.Error("invalid issuer url for wallet initiated flow", log.WithURL(issuerURL)) - return nil, errors.New("invalid issuer url") + return nil, ErrInvalidIssuerURL } profileID, profileVersion := matches[2], matches[3] @@ -287,17 +290,22 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( return nil, fmt.Errorf("wallet initiated flow get profile: %w", err) } - if !profile.OIDCConfig.WalletInitiatedAuthFlowSupported { + if profile.OIDCConfig == nil || !profile.OIDCConfig.WalletInitiatedAuthFlowSupported { return nil, errors.New("wallet initiated auth flow is not supported for current profile") } + if profile.OIDCConfig.ClaimsEndpoint == "" { + return nil, errors.New("empty claims endpoint for profile") + } + if len(profile.CredentialTemplates) == 0 { + return nil, errors.New("no credential templates configured") + } oidcConfig, err := s.wellKnownService.GetOIDCConfiguration(ctx, profile.OIDCConfig.IssuerWellKnownURL) if err != nil { return nil, fmt.Errorf("wallet initiated flow get oidc config: %w", err) } - // todo: what scopes should I use? In code below I'm using scopes from incoming request but removing issuerURL - scopes := make([]string, 0, len(requestScopes)-1) + var scopes []string for _, scope := range requestScopes { if scope == issuerURL { @@ -320,29 +328,14 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( return nil, err } - var credTemplate string // todo Sudesh remove hardcode - var credType string - - switch profile.ID { // todo Sudesh remove hardcode - case "bank_issuer": - credTemplate = "universityDegreeTemplateID" - credType = "UniversityDegreeCredential" - case "i_myprofile_ud_es256k_jwt": - credTemplate = "permanentResidentCardTemplateID" - credType = "PermanentResidentCard" - default: - credTemplate = "crudeProductCredentialTemplateID" - credType = "CrudeProductCredential" - } - return &PrepareClaimDataAuthorizationResponse{ WalletInitiatedFlow: &common.WalletInitiatedFlowData{ - ProfileId: profileID, - ProfileVersion: profileVersion, - ClaimEndpoint: fmt.Sprintf( // todo Sudesh remove hardcode - "https://mock-login-consent.example.com:8099/claim-data?credentialType=%v", credType), - CredentialTemplateId: credTemplate, // todo Sudesh remove hardcode + ProfileId: profileID, + ProfileVersion: profileVersion, + ClaimEndpoint: profile.OIDCConfig.ClaimsEndpoint, + CredentialTemplateId: profile.CredentialTemplates[0].ID, OpState: opState, + Scopes: &scopes, }, ProfileID: profileID, ProfileVersion: profileVersion, diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go index be0be17b2..e42bc5c3e 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go @@ -13,9 +13,12 @@ import ( "github.com/golang/mock/gomock" "github.com/google/uuid" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/trustbloc/vcs/pkg/event/spi" + profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) @@ -141,3 +144,149 @@ func TestStoreAuthCode(t *testing.T) { assert.NotEqual(t, tx.ID, resp) }) } + +func TestInitiateWalletFlowFromStoreCode(t *testing.T) { + t.Run("profile not found", func(t *testing.T) { + store := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: store, + EventService: eventMock, + EventTopic: spi.IssuerEventTopic, + ProfileService: profileSvc, + WellKnownService: wellKnown, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(gomock.Any(), gomock.Any()). + Return(nil, errors.New("issuer not found")) + resp, err := srv.StoreAuthorizationCode(context.TODO(), "random-op-state", "code123", + &common.WalletInitiatedFlowData{ + OpState: "random-op-state", + ProfileId: "bank_issuer1", + ProfileVersion: "v111.0", + }, + ) + assert.Empty(t, resp) + assert.ErrorContains(t, err, "issuer not found") + }) + + t.Run("error init", func(t *testing.T) { + store := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: store, + EventService: eventMock, + EventTopic: spi.IssuerEventTopic, + ProfileService: profileSvc, + WellKnownService: wellKnown, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), "v111.0"). + Return(&profileapi.Issuer{ + CredentialTemplates: []*profileapi.CredentialTemplate{ + { + ID: "some-template", + }, + }, + Active: false, + VCConfig: &profileapi.VCConfig{}, + SigningDID: &profileapi.SigningDID{}, + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + IssuerWellKnownURL: "https://awesome.local", + ClaimsEndpoint: "https://awesome.claims.local", + GrantTypesSupported: []string{ + "authorization_code", + }, + ScopesSupported: []string{ + "scope1", + "scope2", + "scope3", + }, + }, + }, nil) + + resp, err := srv.StoreAuthorizationCode(context.TODO(), "random-op-state", "code123", + &common.WalletInitiatedFlowData{ + OpState: "random-op-state", + ProfileId: "bank_issuer1", + ProfileVersion: "v111.0", + }, + ) + assert.Empty(t, resp) + assert.ErrorContains(t, err, "can not initiate issuance for wallet flow. profile not active") + }) + t.Run("success", func(t *testing.T) { + store := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + srv, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: store, + EventService: eventMock, + EventTopic: spi.IssuerEventTopic, + ProfileService: profileSvc, + WellKnownService: wellKnown, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), "v111.0"). + Return(&profileapi.Issuer{ + CredentialTemplates: []*profileapi.CredentialTemplate{ + { + ID: "some-template", + }, + }, + Active: true, + VCConfig: &profileapi.VCConfig{}, + SigningDID: &profileapi.SigningDID{}, + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + IssuerWellKnownURL: "https://awesome.local", + ClaimsEndpoint: "https://awesome.claims.local", + GrantTypesSupported: []string{ + "authorization_code", + }, + ScopesSupported: []string{ + "scope1", + "scope2", + "scope3", + }, + }, + }, nil) + + store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()). + Return(&oidc4ci.Transaction{}, nil) + wellKnown.EXPECT().GetOIDCConfiguration(gomock.Any(), gomock.Any()). + Return(&oidc4ci.OIDCConfiguration{}, nil) + eventMock.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + store.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + + resp, err := srv.StoreAuthorizationCode(context.TODO(), "random-op-state", "code123", + &common.WalletInitiatedFlowData{ + ClaimEndpoint: "", + CredentialTemplateId: "", + OpState: "random-op-state", + ProfileId: "bank_issuer1", + ProfileVersion: "v111.0", + Scopes: lo.ToPtr([]string{ + "scope1", + "scope2", + "scope3", + }), + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, resp) + }) +} diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index 6988f3423..adf92bde0 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -503,6 +503,358 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { } } +func TestPrepareClaimDataAuthorizationForWalletFlow(t *testing.T) { + t.Run("invalid url", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1", + "scope3", + }, + }, + ) + assert.Nil(t, resp) + assert.ErrorContains(t, err, "data not found") + }) + + t.Run("profile not found", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(nil, errors.New("not found")) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "wallet initiated flow get profile") + }) + t.Run("profile wallet flow not supported", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(&profileapi.Issuer{}, nil) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "wallet initiated auth flow is not supported for current profile") + }) + t.Run("profile wallet flow claims url missing", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(&profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + }, + }, nil) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "empty claims endpoint for profile") + }) + t.Run("profile wallet flow credential templates are missing", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(&profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + ClaimsEndpoint: "sadsadsa", + }, + }, nil) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "no credential templates configured") + }) + + t.Run("profile wallet flow well-known err", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(&profileapi.Issuer{ + CredentialTemplates: []*profileapi.CredentialTemplate{ + { + ID: "123", + }, + }, + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + ClaimsEndpoint: "sadsadsa", + }, + }, nil) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + wellKnown.EXPECT().GetOIDCConfiguration(gomock.Any(), gomock.Any()). + Return(nil, errors.New("well-known err")) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "well-known err") + }) + t.Run("profile wallet flow event err", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), gomock.Any()). + Return(&profileapi.Issuer{ + CredentialTemplates: []*profileapi.CredentialTemplate{ + { + ID: "123", + }, + }, + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + ClaimsEndpoint: "sadsadsa", + }, + }, nil) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + wellKnown.EXPECT().GetOIDCConfiguration(gomock.Any(), gomock.Any()). + Return(&oidc4ci.OIDCConfiguration{}, nil) + eventMock.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()). + Return(errors.New("publish err")) + eventMock.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, event ...*spi.Event) error { + assert.Equal(t, "vcs-issuer", topic) + assert.Equal(t, spi.IssuerOIDCInteractionFailed, event[0].Type) + return nil + }) + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v1.0", + "scope3", + }, + }, + ) + + assert.Nil(t, resp) + assert.ErrorContains(t, err, "publish err") + }) + t.Run("success", func(t *testing.T) { + mockTransactionStore := NewMockTransactionStore(gomock.NewController(t)) + eventMock := NewMockEventService(gomock.NewController(t)) + profileSvc := NewMockProfileService(gomock.NewController(t)) + wellKnown := NewMockWellKnownService(gomock.NewController(t)) + + svc, err := oidc4ci.NewService(&oidc4ci.Config{ + TransactionStore: mockTransactionStore, + EventService: eventMock, + ProfileService: profileSvc, + WellKnownService: wellKnown, + EventTopic: spi.IssuerEventTopic, + }) + assert.NoError(t, err) + + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). + Return(nil, oidc4ci.ErrDataNotFound) + wellKnown.EXPECT().GetOIDCConfiguration(gomock.Any(), "https://awesome.local"). + Return(&oidc4ci.OIDCConfiguration{}, nil) + + eventMock.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, topic string, event ...*spi.Event) error { + assert.Equal(t, "vcs-issuer", topic) + assert.Len(t, event, 1) + + assert.Equal(t, spi.IssuerOIDCInteractionAuthorizationRequestPrepared, event[0].Type) + return nil + }) + profileSvc.EXPECT().GetProfile(profileapi.ID("bank_issuer1"), "v111.0"). + Return(&profileapi.Issuer{ + CredentialTemplates: []*profileapi.CredentialTemplate{ + { + ID: "some-template", + }, + }, + OIDCConfig: &profileapi.OIDCConfig{ + WalletInitiatedAuthFlowSupported: true, + IssuerWellKnownURL: "https://awesome.local", + ClaimsEndpoint: "https://awesome.claims.local", + }, + }, nil) + + resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), + &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "random-op-state", + Scope: []string{ + "scope1", + "scope2", + "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer1/v111.0", + "scope3", + }, + }, + ) + + assert.NoError(t, err) + assert.NotNil(t, resp) + + assert.Equal(t, "bank_issuer1", resp.ProfileID) + assert.Equal(t, "v111.0", resp.ProfileVersion) + assert.Equal(t, []string{ + "scope1", + "scope2", + "scope3", + }, resp.Scope) + assert.Equal(t, resp.Scope, *resp.WalletInitiatedFlow.Scopes) + assert.Equal(t, "https://awesome.claims.local", resp.WalletInitiatedFlow.ClaimEndpoint) + assert.Equal(t, "random-op-state", resp.WalletInitiatedFlow.OpState) + assert.Equal(t, "bank_issuer1", resp.WalletInitiatedFlow.ProfileId) + assert.Equal(t, "v111.0", resp.WalletInitiatedFlow.ProfileVersion) + assert.Equal(t, "some-template", resp.WalletInitiatedFlow.CredentialTemplateId) + }) +} + func TestValidatePreAuthCode(t *testing.T) { t.Run("success with pin", func(t *testing.T) { storeMock := NewMockTransactionStore(gomock.NewController(t)) diff --git a/test/bdd/fixtures/profile/profiles.json b/test/bdd/fixtures/profile/profiles.json index 4d2dfba9e..4efe96133 100644 --- a/test/bdd/fixtures/profile/profiles.json +++ b/test/bdd/fixtures/profile/profiles.json @@ -167,16 +167,17 @@ ], "enable_dynamic_client_registration": true, "wallet_initiated_auth_flow_supported": true, - "pre-authorized_grant_anonymous_access_supported": true + "pre-authorized_grant_anonymous_access_supported": true, + "claims_endpoint": "https://mock-login-consent.example.com:8099/claim-data?credentialType=PermanentResidentCard" }, "credentialTemplates": [ { "contexts": [ "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" + "https://w3id.org/citizenship/v1" ], - "type": "VerifiedEmployee", - "id": "templateID", + "type": "PermanentResidentCard", + "id": "permanentResidentCardTemplateID", "issuer": "did:orb:i_myprofile_ud_es256k_jwt", "checks": { "strict": false @@ -187,8 +188,8 @@ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "UniversityDegreeCredential", - "id": "universityDegreeTemplateID", + "type": "VerifiedEmployee", + "id": "templateID", "issuer": "did:orb:i_myprofile_ud_es256k_jwt", "checks": { "strict": false @@ -197,10 +198,10 @@ { "contexts": [ "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/citizenship/v1" + "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "PermanentResidentCard", - "id": "permanentResidentCardTemplateID", + "type": "UniversityDegreeCredential", + "id": "universityDegreeTemplateID", "issuer": "did:orb:i_myprofile_ud_es256k_jwt", "checks": { "strict": false @@ -514,16 +515,17 @@ ], "enable_dynamic_client_registration": true, "pre-authorized_grant_anonymous_access_supported": true, - "wallet_initiated_auth_flow_supported": true + "wallet_initiated_auth_flow_supported": true, + "claims_endpoint": "https://mock-login-consent.example.com:8099/claim-data?credentialType=CrudeProductCredential" }, "credentialTemplates": [ { "contexts": [ "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" + "https://trustbloc.github.io/context/vc/examples-crude-product-v1.jsonld" ], - "type": "VerifiedEmployee", - "id": "templateID", + "type": "CrudeProductCredential", + "id": "crudeProductCredentialTemplateID", "issuer": "did:jwk:i_myprofile_cmtr_p256_ldp", "checks": { "strict": true @@ -534,8 +536,8 @@ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "UniversityDegreeCredential", - "id": "universityDegreeTemplateID", + "type": "VerifiedEmployee", + "id": "templateID", "issuer": "did:jwk:i_myprofile_cmtr_p256_ldp", "checks": { "strict": true @@ -544,10 +546,10 @@ { "contexts": [ "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/citizenship/v1" + "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "PermanentResidentCard", - "id": "permanentResidentCardTemplateID", + "type": "UniversityDegreeCredential", + "id": "universityDegreeTemplateID", "issuer": "did:jwk:i_myprofile_cmtr_p256_ldp", "checks": { "strict": true @@ -556,10 +558,10 @@ { "contexts": [ "https://www.w3.org/2018/credentials/v1", - "https://trustbloc.github.io/context/vc/examples-crude-product-v1.jsonld" + "https://w3id.org/citizenship/v1" ], - "type": "CrudeProductCredential", - "id": "crudeProductCredentialTemplateID", + "type": "PermanentResidentCard", + "id": "permanentResidentCardTemplateID", "issuer": "did:jwk:i_myprofile_cmtr_p256_ldp", "checks": { "strict": true @@ -692,7 +694,8 @@ ], "enable_dynamic_client_registration": true, "pre-authorized_grant_anonymous_access_supported": true, - "wallet_initiated_auth_flow_supported": true + "wallet_initiated_auth_flow_supported": true, + "claims_endpoint": "https://mock-login-consent.example.com:8099/claim-data?credentialType=UniversityDegreeCredential" }, "credentialTemplates": [ { @@ -700,8 +703,8 @@ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "VerifiedEmployee", - "id": "templateID", + "type": "UniversityDegreeCredential", + "id": "universityDegreeTemplateID", "issuer": "did:orb:bank_issuer", "checks": { "strict": true @@ -712,8 +715,8 @@ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1" ], - "type": "UniversityDegreeCredential", - "id": "universityDegreeTemplateID", + "type": "VerifiedEmployee", + "id": "templateID", "issuer": "did:orb:bank_issuer", "checks": { "strict": true From 969ac600630346cca29785ed7b45071a875a5a3c Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 17:37:24 +0200 Subject: [PATCH 13/22] fix: comments Signed-off-by: Stas D --- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 14 +++++++------- .../walletrunner/wallet_runner_oidc4ci_pre_auth.go | 2 +- docs/v1/openapi.yaml | 2 +- pkg/restapi/v1/util/validate.go | 2 +- pkg/restapi/v1/util/validate_test.go | 2 +- pkg/service/oidc4ci/oidc4ci_service.go | 2 +- .../oidc4ci/oidc4ci_service_store_auth_code.go | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index bfb94e068..53a4a7110 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -89,7 +89,7 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { offerResponse.CredentialIssuer, ) if err != nil { - return fmt.Errorf("get issuer oidc issuer config: %w", err) + return fmt.Errorf("get issuer OIDC issuer config: %w", err) } redirectURL, err := url.Parse(config.RedirectURI) @@ -234,25 +234,25 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { var matchRegex = regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex) -func extractIssuerUrlFromScopes(scopes []string) (string, error) { +func extractIssuerURLFromScopes(scopes []string) (string, error) { for _, scope := range scopes { if matchRegex.MatchString(scope) { return scope, nil } } - return "", errors.New("issuer url not found in scopes") + return "", errors.New("issuer URL not found in scopes") } func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow Wallet initiated") ctx := context.Background() - // Check whether scope contains combined string Issuer URL||ClaimEndpoint||credentialTemplateID - issuerUrl, err := extractIssuerUrlFromScopes(config.Scope) + + issuerUrl, err := extractIssuerURLFromScopes(config.Scope) if err != nil { return errors.New( "undefined scopes supplied. " + - "Make sure one of the provided scope is an vcs issuer url format. ref " + + "Make sure one of the provided scope is in the VCS issuer URL format. ref " + oidc4ci.WalletInitFlowClaimRegex) } @@ -260,7 +260,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) issuerUrl, ) if err != nil { - return fmt.Errorf("get issuer oidc issuer config: %w", err) + return fmt.Errorf("get issuer OIDC issuer config: %w", err) } oidcConfig, err := s.getIssuerOIDCConfig(ctx, issuerUrl) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go index 565b009e7..75347fa54 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci_pre_auth.go @@ -53,7 +53,7 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti s.perfInfo.GetIssuerCredentialsOIDCConfig = time.Since(startTime) if err != nil { - return nil, fmt.Errorf("get issuer oidc issuer config: %w", err) + return nil, fmt.Errorf("get issuer OIDC issuer config: %w", err) } tokenEndpoint := oidcConfig.TokenEndpoint diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 532417308..1a990ab75 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -802,7 +802,7 @@ components: description: JSON Boolean indicating whether the issuer accepts a Token Request with a Pre-Authorized Code but without a client id. The default is false. wallet_initiated_auth_flow_supported: type: boolean - description: JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. + description: JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4CI. The default is false. required: - authorization_endpoint - token_endpoint diff --git a/pkg/restapi/v1/util/validate.go b/pkg/restapi/v1/util/validate.go index a3d005837..f59f172b4 100644 --- a/pkg/restapi/v1/util/validate.go +++ b/pkg/restapi/v1/util/validate.go @@ -1,5 +1,5 @@ /* -Copyright SecureKey Technologies Inc. All Rights Reserved. +Copyright Avast Software. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ diff --git a/pkg/restapi/v1/util/validate_test.go b/pkg/restapi/v1/util/validate_test.go index 471dbd799..52497c134 100644 --- a/pkg/restapi/v1/util/validate_test.go +++ b/pkg/restapi/v1/util/validate_test.go @@ -1,5 +1,5 @@ /* -Copyright SecureKey Technologies Inc. All Rights Reserved. +Copyright Avast Software. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 7f50053a1..861871bce 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -205,7 +205,7 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( s.extractIssuerURLFromClaims(req.Scope), req.OpState, ) - if walletFlowErr != nil && errors.Is(walletFlowErr, ErrInvalidIssuerURL) { // not wallet flow + if walletFlowErr != nil && errors.Is(walletFlowErr, ErrInvalidIssuerURL) { // not wallet-initiated flow return nil, err } diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go index fa348e77a..c17964363 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code.go @@ -25,7 +25,7 @@ func (s *Service) StoreAuthorizationCode( ) (TxID, error) { var tx *Transaction var err error - if flowData != nil { // its wallet initiated issuance, first we need to initiate issuance + if flowData != nil { // it's wallet initiated issuance, first we need to initiate issuance tx, err = s.initiateIssuanceWithWalletFlow(ctx, flowData) } else { tx, err = s.store.FindByOpState(ctx, opState) @@ -75,7 +75,7 @@ func (s *Service) initiateIssuanceWithWalletFlow( WalletInitiatedIssuance: true, }, profile) if err != nil { - return nil, fmt.Errorf("can not initiate issuance for wallet flow. %w", err) + return nil, fmt.Errorf("can not initiate issuance for wallet-initiated flow. %w", err) } return tx.Tx, nil From abc9b5516c1fd8594425d24473e3e768bbac24d9 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 17:48:50 +0200 Subject: [PATCH 14/22] fix: test Signed-off-by: Stas D --- pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go index e42bc5c3e..07b3a913a 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_store_auth_code_test.go @@ -222,7 +222,8 @@ func TestInitiateWalletFlowFromStoreCode(t *testing.T) { }, ) assert.Empty(t, resp) - assert.ErrorContains(t, err, "can not initiate issuance for wallet flow. profile not active") + assert.ErrorContains(t, err, + "can not initiate issuance for wallet-initiated flow. profile not active") }) t.Run("success", func(t *testing.T) { store := NewMockTransactionStore(gomock.NewController(t)) From 3e9a980aaf15355d9e8f61e72cac0adb962d1f1a Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 18:06:04 +0200 Subject: [PATCH 15/22] chore: openapigen Signed-off-by: Stas D --- api/spec/openapi.gen.go | 106 +++++++++--------- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 6 +- pkg/restapi/v1/issuer/openapi.gen.go | 2 +- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 9e8b916a9..d4a09f473 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -101,59 +101,59 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index 53a4a7110..75c1b97e2 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -82,7 +82,7 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { s.print("Getting issuer OIDC config") oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer) if err != nil { - return fmt.Errorf("get issuer oidc config: %w", err) + return fmt.Errorf("get issuer OIDC config: %w", err) } oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig( @@ -265,7 +265,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) oidcConfig, err := s.getIssuerOIDCConfig(ctx, issuerUrl) if err != nil { - return fmt.Errorf("get issuer oidc config: %w", err) + return fmt.Errorf("get issuer OIDC config: %w", err) } redirectURL, err := url.Parse(config.RedirectURI) @@ -392,7 +392,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) verifiable.WithJSONLDDocumentLoader( s.ariesServices.JSONLDDocumentLoader())) if err != nil { - return fmt.Errorf("parse vc: %w", err) + return fmt.Errorf("parse VC: %w", err) } log.Printf( diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index ba9aebb64..f8b057f7b 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -289,7 +289,7 @@ type WellKnownOpenIDConfiguration struct { // URL of the OP's OAuth 2.0 Token Endpoint. TokenEndpoint string `json:"token_endpoint"` - // JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4VCI. The default is false. + // JSON Boolean indicating whether the issuer profile supports wallet initiated flow in OIDC4CI. The default is false. WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"` } From 94b6f44ddadaa6328eaae0a971d10c0284a603a6 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 18:19:12 +0200 Subject: [PATCH 16/22] fix: auth flow bdd Signed-off-by: Stas D --- test/bdd/features/oidc4vc_api.feature | 2 +- test/bdd/pkg/v1/oidc4vc/steps.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/bdd/features/oidc4vc_api.feature b/test/bdd/features/oidc4vc_api.feature index 87f885d0b..d7a8e9e0e 100644 --- a/test/bdd/features/oidc4vc_api.feature +++ b/test/bdd/features/oidc4vc_api.feature @@ -84,7 +84,7 @@ Feature: OIDC4VC REST API Given Organization "test_org" has been authorized with client id "f13d1va9lp403pb9lyj89vk55" and secret "ejqxi9jb1vew2jbdnogpjcgrz" And Issuer with id "" is authorized as a Profile user And User holds credential "" with templateID "" - When User interacts with Wallet to initiate credential issuance using authorization code flow + When User interacts with Wallet to initiate credential issuance using authorization code flow with wallet-initiated Then credential is issued Then User interacts with Verifier and initiate OIDC4VP interaction under "" profile for organization "test_org" with presentation definition ID "" and fields "" And Verifier form organization "test_org" waits for interaction succeeded event diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 9d4b76388..b1a04c403 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -89,7 +89,8 @@ func (s *Steps) RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^credential is issued$`, s.checkIssuedCredential) // CI. - sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow$`, s.runOIDC4CIAuthWalletInitiatedFlow) + sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow$`, s.runOIDC4CIAuth) + sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow with wallet-initiated$`, s.runOIDC4CIAuthWalletInitiatedFlow) sc.Step(`^User interacts with Wallet to initiate credential issuance using authorization code flow with dynamic client registration$`, s.runOIDC4CIAuthWithDynamicClient) sc.Step(`^User interacts with Wallet to initiate credential issuance using pre authorization code flow$`, s.runOIDC4CIPreAuthWithValidClaims) From 1be8b9118950a487db4ba896cdfc21ab566b6922 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 19:16:04 +0200 Subject: [PATCH 17/22] chore: increase time Signed-off-by: Stas D --- test/bdd/bddtests_test.go | 2 +- test/bdd/pkg/v1/oidc4vc/steps.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/bdd/bddtests_test.go b/test/bdd/bddtests_test.go index b2c3c0534..36e6c147f 100644 --- a/test/bdd/bddtests_test.go +++ b/test/bdd/bddtests_test.go @@ -103,7 +103,7 @@ func beforeSuiteHook() { logger.Fatal("bdd test beforeSuiteHook", logfields.WithCommand(string(out)), log.WithError(err)) } - testSleep := 60 + testSleep := 80 if os.Getenv("TEST_SLEEP") != "" { s, err := strconv.Atoi(os.Getenv("TEST_SLEEP")) diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index b1a04c403..40b078299 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -62,7 +62,6 @@ func NewSteps(ctx *bddcontext.BDDContext) (*Steps, error) { c.DidKeyType = "ECDSAP384DER" c.DidMethod = "orb" c.KeepWalletOpen = true - c.Debug = true }) if err != nil { return nil, fmt.Errorf("unable create wallet runner: %w", err) From c1e38ff36c10a6005428ceb6095db5a140e28774 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 19:48:46 +0200 Subject: [PATCH 18/22] feat: add reset Signed-off-by: Stas D --- test/bdd/bddtests_test.go | 2 +- test/bdd/pkg/v1/oidc4vc/oidc4ci.go | 4 +++ test/bdd/pkg/v1/oidc4vc/steps.go | 54 +++++++++++++++++++++--------- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/test/bdd/bddtests_test.go b/test/bdd/bddtests_test.go index 36e6c147f..b2c3c0534 100644 --- a/test/bdd/bddtests_test.go +++ b/test/bdd/bddtests_test.go @@ -103,7 +103,7 @@ func beforeSuiteHook() { logger.Fatal("bdd test beforeSuiteHook", logfields.WithCommand(string(out)), log.WithError(err)) } - testSleep := 80 + testSleep := 60 if os.Getenv("TEST_SLEEP") != "" { s, err := strconv.Atoi(os.Getenv("TEST_SLEEP")) diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go index ed826e8e4..fba1f1316 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4ci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4ci.go @@ -41,6 +41,10 @@ const ( ) func (s *Steps) authorizeIssuer(profileVersionedID string) error { + if err := s.ResetAndSetup(); err != nil { + return err + } + issuer, ok := s.bddContext.IssuerProfiles[profileVersionedID] if !ok { return fmt.Errorf("issuer profile '%s' not found", profileVersionedID) diff --git a/test/bdd/pkg/v1/oidc4vc/steps.go b/test/bdd/pkg/v1/oidc4vc/steps.go index 40b078299..bf789740e 100644 --- a/test/bdd/pkg/v1/oidc4vc/steps.go +++ b/test/bdd/pkg/v1/oidc4vc/steps.go @@ -46,15 +46,25 @@ type Steps struct { oidc4vpHooks *walletrunner.OIDC4VPHooks } -// NewSteps returns new Steps context. -func NewSteps(ctx *bddcontext.BDDContext) (*Steps, error) { +func (s *Steps) ResetAndSetup() error { + s.tlsConfig = nil + s.cookie = nil + s.issuerProfile = nil + s.verifierProfile = nil + s.walletRunner = nil + s.dl = nil + s.issuedCredentialType = "" + s.issuedCredentialTemplateID = "" + s.vpClaimsTransactionID = "" + s.presentationDefinitionID = "" + s.usersNum = 0 + s.concurrentReq = 0 + s.stressResult = nil + s.oidc4vpHooks = nil + jar, err := cookiejar.New(&cookiejar.Options{}) if err != nil { - return nil, fmt.Errorf("init cookie jar: %w", err) - } - - tlsConf := &tls.Config{ - InsecureSkipVerify: true, + return fmt.Errorf("init cookie jar: %w", err) } walletRunner, err := walletrunner.New(vcprovider.ProviderVCS, @@ -64,21 +74,35 @@ func NewSteps(ctx *bddcontext.BDDContext) (*Steps, error) { c.KeepWalletOpen = true }) if err != nil { - return nil, fmt.Errorf("unable create wallet runner: %w", err) + return fmt.Errorf("unable create wallet runner: %w", err) } loader, err := bddutil.DocumentLoader() if err != nil { + return err + } + + s.cookie = jar + s.tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + s.walletRunner = walletRunner + s.dl = loader + + return nil +} + +// NewSteps returns new Steps context. +func NewSteps(ctx *bddcontext.BDDContext) (*Steps, error) { + s := &Steps{ + bddContext: ctx, + } + + if err := s.ResetAndSetup(); err != nil { return nil, err } - return &Steps{ - bddContext: ctx, - cookie: jar, - tlsConfig: tlsConf, - walletRunner: walletRunner, - dl: loader, - }, nil + return s, nil } // RegisterSteps registers OIDC4VC scenario steps. From 4629da33eda492c56bf5521433ee5c5e1f26b6c2 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 21:00:03 +0200 Subject: [PATCH 19/22] chore: make issuer state optional Signed-off-by: Stas D --- api/spec/openapi.gen.go | 270 +++++++++--------- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 2 - docs/v1/openapi.yaml | 2 +- pkg/restapi/v1/oidc4ci/controller.go | 15 +- pkg/restapi/v1/oidc4ci/controller_test.go | 44 +-- pkg/restapi/v1/oidc4ci/openapi.gen.go | 6 +- 6 files changed, 172 insertions(+), 167 deletions(-) diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index d4a09f473..2b562a35e 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,141 +19,141 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index 75c1b97e2..3ed605d82 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -299,7 +299,6 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) }, } - issuerState := uuid.New().String() state := uuid.New().String() b, err := json.Marshal(&common.AuthorizationDetails{ @@ -315,7 +314,6 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) } authCodeURL := s.oauthClient.AuthCodeURL(state, - oauth2.SetAuthURLParam("issuer_state", issuerState), oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"), oauth2.SetAuthURLParam("code_challenge_method", "S256"), oauth2.SetAuthURLParam("authorization_details", string(b)), diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index 1a990ab75..f69c224c6 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -623,7 +623,7 @@ paths: in: query name: issuer_state description: 'String value identifying a certain processing context at the credential issuer. A value for this parameter is typically passed in an issuance initiation request from the issuer to the wallet. This request parameter is used to pass the issuer_state value back to the credential issuer. The issuer must take into account that op_state is not guaranteed to originate from this issuer, could be an attack.' - required: true + required: false tags: - oidc4ci parameters: [] diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index 4d28d2768..73346afc4 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -24,6 +24,7 @@ import ( "time" gojose "github.com/go-jose/go-jose/v3" + "github.com/google/uuid" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" "github.com/labstack/echo/v4" @@ -193,6 +194,10 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e req := e.Request() ctx := req.Context() + if lo.FromPtr(params.IssuerState) == "" { + params.IssuerState = lo.ToPtr(uuid.NewString()) + } + ar, err := c.oauth2Provider.NewAuthorizeRequest(ctx, req) if err != nil { return resterr.NewFositeError(resterr.FositeAuthorizeError, e, c.oauth2Provider, err).WithAuthorizeRequester(ar) @@ -200,7 +205,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e ses := &fosite.DefaultSession{ Extra: map[string]interface{}{ - sessionOpStateKey: params.IssuerState, + sessionOpStateKey: lo.FromPtr(params.IssuerState), authorizationDetailsKey: lo.FromPtr(params.AuthorizationDetails), }, } @@ -238,7 +243,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e Types: credentialType, Format: vcFormat, }, - OpState: params.IssuerState, + OpState: lo.FromPtr(params.IssuerState), ResponseType: params.ResponseType, Scope: lo.ToPtr(scope), }, @@ -273,7 +278,7 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e if err = c.stateStore.SaveAuthorizeState( ctx, - params.IssuerState, + lo.FromPtr(params.IssuerState), &oidc4ci.AuthorizeState{ RedirectURI: ar.GetRedirectURI(), RespondMode: string(ar.GetResponseMode()), @@ -301,13 +306,13 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e authCodeURL, err = c.buildAuthCodeURLWithPAR(ctx, oauthConfig, *claimDataAuth.PushedAuthorizationRequestEndpoint, - params.IssuerState, + lo.FromPtr(params.IssuerState), ) if err != nil { return err } } else { - authCodeURL = oauthConfig.AuthCodeURL(params.IssuerState) + authCodeURL = oauthConfig.AuthCodeURL(lo.FromPtr(params.IssuerState)) } return e.Redirect(http.StatusSeeOther, authCodeURL) diff --git a/pkg/restapi/v1/oidc4ci/controller_test.go b/pkg/restapi/v1/oidc4ci/controller_test.go index 72483f57f..2bafa5545 100644 --- a/pkg/restapi/v1/oidc4ci/controller_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_test.go @@ -218,7 +218,7 @@ func TestController_OidcAuthorize(t *testing.T) { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", State: &state, - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), AuthorizationDetails: lo.ToPtr(`{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`), } @@ -252,7 +252,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -261,7 +261,8 @@ func TestController_OidcAuthorize(t *testing.T) { }, nil }) - mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), params.IssuerState, gomock.Any()).Return(nil) + mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), *params.IssuerState, gomock.Any()). + Return(nil) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.NoError(t, err) @@ -274,7 +275,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -311,7 +312,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -321,7 +322,7 @@ func TestController_OidcAuthorize(t *testing.T) { }, ) - mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), params.IssuerState, gomock.Any()).Return(nil) + mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), *params.IssuerState, gomock.Any()).Return(nil) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.NoError(t, err) @@ -334,7 +335,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -370,7 +371,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -380,7 +381,8 @@ func TestController_OidcAuthorize(t *testing.T) { }, ) - mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), params.IssuerState, gomock.Any()).Return(nil) + mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), *params.IssuerState, gomock.Any()). + Return(nil) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { assert.ErrorContains(t, err, "unexpected status code") @@ -391,7 +393,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } mockOAuthProvider.EXPECT().NewAuthorizeRequest(gomock.Any(), gomock.Any()).Return(nil, @@ -406,7 +408,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), AuthorizationDetails: lo.ToPtr("invalid"), } @@ -427,7 +429,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), AuthorizationDetails: lo.ToPtr(`{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"invalid"}`), } @@ -446,7 +448,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -462,7 +464,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return nil, errors.New("prepare claim data authorization error") @@ -477,7 +479,7 @@ func TestController_OidcAuthorize(t *testing.T) { setup: func() { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -493,7 +495,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -514,7 +516,7 @@ func TestController_OidcAuthorize(t *testing.T) { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", State: &state, - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -546,7 +548,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -567,7 +569,7 @@ func TestController_OidcAuthorize(t *testing.T) { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", State: &state, - IssuerState: "opState", + IssuerState: lo.ToPtr("opState"), } scope := []string{"openid", "profile"} @@ -599,7 +601,7 @@ func TestController_OidcAuthorize(t *testing.T) { reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { assert.Equal(t, params.ResponseType, req.ResponseType) - assert.Equal(t, params.IssuerState, req.OpState) + assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) return &http.Response{ @@ -608,7 +610,7 @@ func TestController_OidcAuthorize(t *testing.T) { }, nil }) - mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), params.IssuerState, gomock.Any()).Return( + mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), *params.IssuerState, gomock.Any()).Return( errors.New("save state error")) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { diff --git a/pkg/restapi/v1/oidc4ci/openapi.gen.go b/pkg/restapi/v1/oidc4ci/openapi.gen.go index ba477bd24..92aa8101f 100644 --- a/pkg/restapi/v1/oidc4ci/openapi.gen.go +++ b/pkg/restapi/v1/oidc4ci/openapi.gen.go @@ -229,7 +229,7 @@ type OidcAuthorizeParams struct { UserHint *string `form:"user_hint,omitempty" json:"user_hint,omitempty"` // String value identifying a certain processing context at the credential issuer. A value for this parameter is typically passed in an issuance initiation request from the issuer to the wallet. This request parameter is used to pass the issuer_state value back to the credential issuer. The issuer must take into account that op_state is not guaranteed to originate from this issuer, could be an attack. - IssuerState string `form:"issuer_state" json:"issuer_state"` + IssuerState *string `form:"issuer_state,omitempty" json:"issuer_state,omitempty"` } // OidcCredentialJSONBody defines parameters for OidcCredential. @@ -356,9 +356,9 @@ func (w *ServerInterfaceWrapper) OidcAuthorize(ctx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter user_hint: %s", err)) } - // ------------- Required query parameter "issuer_state" ------------- + // ------------- Optional query parameter "issuer_state" ------------- - err = runtime.BindQueryParameter("form", true, true, "issuer_state", ctx.QueryParams(), ¶ms.IssuerState) + err = runtime.BindQueryParameter("form", true, false, "issuer_state", ctx.QueryParams(), ¶ms.IssuerState) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter issuer_state: %s", err)) } From 00a6c53882c81eefb487b83de192bf192fe6ea21 Mon Sep 17 00:00:00 2001 From: Stas Dmytryshyn Date: Tue, 18 Jul 2023 21:12:40 +0200 Subject: [PATCH 20/22] Update component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go Co-authored-by: Derek Trider --- component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index 3ed605d82..4189a70f2 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -252,7 +252,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) if err != nil { return errors.New( "undefined scopes supplied. " + - "Make sure one of the provided scope is in the VCS issuer URL format. ref " + + "Make sure one of the provided scopes is in the VCS issuer URL format oidc4ci.WalletInitFlowClaimRegex) } From b1466ca3d29780d4f8482c896c4bb0dde8295d92 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 21:22:44 +0200 Subject: [PATCH 21/22] fix: compile Signed-off-by: Stas D --- component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index 4189a70f2..2f2a5fcbf 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -252,7 +252,7 @@ func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) if err != nil { return errors.New( "undefined scopes supplied. " + - "Make sure one of the provided scopes is in the VCS issuer URL format + "Make sure one of the provided scopes is in the VCS issuer URL format. " + oidc4ci.WalletInitFlowClaimRegex) } From 19e03b59f502040d1cf179037f9379b16c2a5eb2 Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Tue, 18 Jul 2023 23:48:43 +0200 Subject: [PATCH 22/22] feat: validate url only Signed-off-by: Stas D --- .../pkg/walletrunner/wallet_runner_oidc4ci.go | 20 ++-------- pkg/service/oidc4ci/oidc4ci_service.go | 21 ++-------- pkg/service/oidc4ci/oidc4ci_service_test.go | 12 +++++- pkg/service/oidc4ci/regex.go | 23 ++++++++++- pkg/service/oidc4ci/regexp_test.go | 40 ------------------- 5 files changed, 39 insertions(+), 77 deletions(-) delete mode 100644 pkg/service/oidc4ci/regexp_test.go diff --git a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go index 2f2a5fcbf..17c51d3e7 100644 --- a/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go +++ b/component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go @@ -17,7 +17,6 @@ import ( "net" "net/http" "net/url" - "regexp" "strings" "time" @@ -232,28 +231,15 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error { return nil } -var matchRegex = regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex) - -func extractIssuerURLFromScopes(scopes []string) (string, error) { - for _, scope := range scopes { - if matchRegex.MatchString(scope) { - return scope, nil - } - } - - return "", errors.New("issuer URL not found in scopes") -} - func (s *Service) RunOIDC4CIWalletInitiated(config *OIDC4CIConfig, hooks *Hooks) error { log.Println("Starting OIDC4VCI authorized code flow Wallet initiated") ctx := context.Background() - issuerUrl, err := extractIssuerURLFromScopes(config.Scope) - if err != nil { + issuerUrl := oidc4ci.ExtractIssuerURLFromScopes(config.Scope) + if issuerUrl == "" { return errors.New( "undefined scopes supplied. " + - "Make sure one of the provided scopes is in the VCS issuer URL format. " + - oidc4ci.WalletInitFlowClaimRegex) + "Make sure one of the provided scopes is in the VCS issuer URL format") } oidcIssuerCredentialConfig, err := s.getIssuerCredentialsOIDCConfig( diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 861871bce..d05370998 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -14,7 +14,6 @@ import ( "errors" "fmt" "net/http" - "regexp" "strings" "time" @@ -202,7 +201,7 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( walletFlowResp, walletFlowErr := s.prepareClaimDataAuthorizationRequestWalletInitiated( ctx, req.Scope, - s.extractIssuerURLFromClaims(req.Scope), + ExtractIssuerURLFromScopes(req.Scope), req.OpState, ) if walletFlowErr != nil && errors.Is(walletFlowErr, ErrInvalidIssuerURL) { // not wallet-initiated flow @@ -259,31 +258,19 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( }, nil } -func (s *Service) extractIssuerURLFromClaims(requestScopes []string) string { - matchRegex := regexp.MustCompile(WalletInitFlowClaimRegex) - - for _, scope := range requestScopes { - if matchRegex.MatchString(scope) { - return scope - } - } - - return "" -} - func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( ctx context.Context, requestScopes []string, issuerURL string, opState string, ) (*PrepareClaimDataAuthorizationResponse, error) { - matches := regexp.MustCompile(WalletInitFlowClaimRegex).FindStringSubmatch(issuerURL) - if len(matches) != WalletInitFlowClaimExpectedMatchCount { + sp := strings.Split(issuerURL, "/") + if len(sp) < WalletInitFlowClaimExpectedMatchCount { logger.Error("invalid issuer url for wallet initiated flow", log.WithURL(issuerURL)) return nil, ErrInvalidIssuerURL } - profileID, profileVersion := matches[2], matches[3] + profileID, profileVersion := sp[len(sp)-2], sp[len(sp)-1] profile, err := s.profileService.GetProfile(profileID, profileVersion) if err != nil { diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index adf92bde0..42e420618 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -519,6 +519,9 @@ func TestPrepareClaimDataAuthorizationForWalletFlow(t *testing.T) { }) assert.NoError(t, err) + profileSvc.EXPECT().GetProfile(profileapi.ID("issuer"), "bank_issuer1"). + Return(nil, errors.New("not found")) + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "random-op-state"). Return(nil, oidc4ci.ErrDataNotFound) resp, err := svc.PrepareClaimDataAuthorizationRequest(context.TODO(), @@ -533,7 +536,7 @@ func TestPrepareClaimDataAuthorizationForWalletFlow(t *testing.T) { }, ) assert.Nil(t, resp) - assert.ErrorContains(t, err, "data not found") + assert.ErrorContains(t, err, "wallet initiated flow get profile: not found") }) t.Run("profile not found", func(t *testing.T) { @@ -1793,3 +1796,10 @@ func TestSelectProperFormat(t *testing.T) { &profileapi.CredentialTemplate{})) }) } + +func TestExtractNoScope(t *testing.T) { + assert.Equal(t, "", oidc4ci.ExtractIssuerURLFromScopes([]string{ + "scope1", + "scope2", + })) +} diff --git a/pkg/service/oidc4ci/regex.go b/pkg/service/oidc4ci/regex.go index 2bf68de85..e849ffd75 100644 --- a/pkg/service/oidc4ci/regex.go +++ b/pkg/service/oidc4ci/regex.go @@ -1,6 +1,25 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + package oidc4ci +import ( + "strings" +) + const ( - WalletInitFlowClaimRegex = `(https|http):\/\/.*\/issuer\/(.*?)\/(.*)$` - WalletInitFlowClaimExpectedMatchCount = 4 + WalletInitFlowClaimExpectedMatchCount = 2 ) + +func ExtractIssuerURLFromScopes(scopes []string) string { + for _, scope := range scopes { + if strings.HasPrefix(scope, "http://") || strings.HasPrefix(scope, "https://") { + return scope + } + } + + return "" +} diff --git a/pkg/service/oidc4ci/regexp_test.go b/pkg/service/oidc4ci/regexp_test.go deleted file mode 100644 index a040722bf..000000000 --- a/pkg/service/oidc4ci/regexp_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package oidc4ci_test - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/trustbloc/vcs/pkg/service/oidc4ci" -) - -func TestRegex(t *testing.T) { - testCases := []struct { - Input string - Version string - ProfileID string - }{ - { - Input: "https://api-gateway.trustbloc.local:5566/issuer/bank_issuer/v1.0", - Version: "v1.0", - ProfileID: "bank_issuer", - }, - { - Input: "https://api-gateway.trustbloc.local:5566/issuer/some-issuer/latest", - Version: "latest", - ProfileID: "some-issuer", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Input, func(t *testing.T) { - rg := regexp.MustCompile(oidc4ci.WalletInitFlowClaimRegex) - matches := rg.FindStringSubmatch(testCase.Input) - profileID, profileVersion := matches[2], matches[3] - - assert.Equal(t, testCase.ProfileID, profileID) - assert.Equal(t, testCase.Version, profileVersion) - }) - } -}