diff --git a/go.mod b/go.mod index f152664..9a60864 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,14 @@ require ( github.com/alecthomas/assert/v2 v2.6.0 github.com/alecthomas/kong v0.9.0 github.com/alecthomas/participle/v2 v2.1.1 - github.com/aws/aws-sdk-go-v2 v1.29.0 - github.com/aws/aws-sdk-go-v2/config v1.27.20 - github.com/aws/aws-sdk-go-v2/credentials v1.17.20 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.3 - github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.0 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.0 - github.com/aws/aws-sdk-go-v2/service/s3 v1.56.0 - github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.0 + github.com/aws/aws-sdk-go-v2 v1.30.0 + github.com/aws/aws-sdk-go-v2/config v1.27.21 + github.com/aws/aws-sdk-go-v2/credentials v1.17.21 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.4 + github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.1 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1 + github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.1 github.com/chzyer/readline v1.5.1 github.com/envoyproxy/go-control-plane v0.12.0 github.com/go-playground/validator/v10 v10.22.0 @@ -47,20 +47,20 @@ require ( github.com/alecthomas/repr v0.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.11 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.11 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.21.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.29.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 // indirect github.com/aws/smithy-go v1.20.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index d3697ae..bf2c286 100644 --- a/go.sum +++ b/go.sum @@ -28,52 +28,52 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/aws/aws-sdk-go-v2 v1.29.0 h1:uMlEecEwgp2gs6CsM6ugquNHr6mg0LHylPBR8u5Ojac= -github.com/aws/aws-sdk-go-v2 v1.29.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2 v1.30.0 h1:6qAwtzlfcTtcL8NHtbDQAqgM5s6NDipQTkPxyH/6kAA= +github.com/aws/aws-sdk-go-v2 v1.30.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= -github.com/aws/aws-sdk-go-v2/config v1.27.20 h1:oQSn/KNUMV54X0FBEDQQ2ymNfcKyMT81ar8gyvMzzqs= -github.com/aws/aws-sdk-go-v2/config v1.27.20/go.mod h1:IbEMotJrWc3Bh7++HXZDlviHZP7kHrkHU3PNl9e17po= -github.com/aws/aws-sdk-go-v2/credentials v1.17.20 h1:VYTCplAeOeBv5InTtrmF61OIwD4aHKryg3KZ6hf7dsI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.20/go.mod h1:ktubcFYvbN8++72jVM9IJoQH6Q2TP+Z7r2VbV1AaESU= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.3 h1:6/r51259lWzcYbkkTJAC3NpWxNJate2AwaSonZa0s34= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.3/go.mod h1:iioQqnZTUUnl9GLahH/2Fd9yMyT1eRzPOdbhEhLkmlI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7 h1:54QUEXjkE1SlxHmRA3gBXA52j/ZSAgdOfAFGv1NsPCY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.7/go.mod h1:bQRjJsdSMzmo/qbtGeBtPbIMp1IgQ+9R9jYJLm12uJA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11 h1:ltkhl3I9ddcRR3Dsy+7bOFFq546O8OYsfNEXVIyuOSE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.11/go.mod h1:H4D8JoCFNJwnT7U5U8iwgG24n71Fx2I/ZP/18eYFr9g= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11 h1:+BgX2AY7yV4ggSwa80z/yZIJX+e0jnNxjMLVyfpSXM0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.11/go.mod h1:DlBATBSDCz30BCdRFldmyLsAzJwi2pdQ+YSdJTHhTUI= +github.com/aws/aws-sdk-go-v2/config v1.27.21 h1:yPX3pjGCe2hJsetlmGNB4Mngu7UPmvWPzzWCv1+boeM= +github.com/aws/aws-sdk-go-v2/config v1.27.21/go.mod h1:4XtlEU6DzNai8RMbjSF5MgGZtYvrhBP/aKZcRtZAVdM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.21 h1:pjAqgzfgFhTv5grc7xPHtXCAaMapzmwA7aU+c/SZQGw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.21/go.mod h1:nhK6PtBlfHTUDVmBLr1dg+WHCOCK+1Fu/WQyVHPsgNQ= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.4 h1:quKxqTZAUmcAqG4afkqGqDzFItix/63gbrSIhS8nkO8= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.14.4/go.mod h1:jyUaxSASxupuTpTZHPFdIo62i78OD7b9pLXHdgYZAJI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8 h1:FR+oWPFb/8qMVYMWN98bUZAGqPvLHiyqg1wqQGfUAXY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.8/go.mod h1:EgSKcHiuuakEIxJcKGzVNWh5srVAQ3jKaSrBGRYvM48= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12 h1:SJ04WXGTwnHlWIODtC5kJzKbeuHt+OUNOgKg7nfnUGw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.12/go.mod h1:FkpvXhA92gb3GE9LD6Og0pHHycTxW7xGpnEh5E7Opwo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12 h1:hb5KgeYfObi5MHkSSZMEudnIvX30iB+E21evI4r6BnQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.12/go.mod h1:CroKe/eWJdyfy9Vx4rljP5wTUjNJfb+fPz1uMYUhEGM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.11 h1:jJ2dythFP5oNunvwc3gBsINl3ZPt/InVm4a5OAr3tag= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.11/go.mod h1:SNkot0zeLtgjP54/6BGuyG12pBcXi77jV5nbEsPgPzg= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.0 h1:Qm7Kp+ljdA8uN+8c2Bc+R5j4llng1RqUMdSXcWWby2I= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.0/go.mod h1:Cve14uutlJBkU6O7VOJzSW/7ouJ5ODiVlsbUrz6hdw0= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.0 h1:PkT1xMKymZEvR8n5WM97XdLWwxQGxnDrqMaquPLI0UY= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.0/go.mod h1:IpoHTdKbzTZUkF67mAGOcqndO7LA8yzMF9FbJbeAKIk= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.0 h1:3x37QZngGIQxEDeJseh1pAZyNjHsqnfjlOA2rnIC+SQ= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.0/go.mod h1:khIrEd+7RlXKVbEkJmJkq7pIopyGfy1JiTsqVIGF83M= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12 h1:DXFWyt7ymx/l1ygdyTTS0X923e+Q2wXIxConJzrgwc0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.12/go.mod h1:mVOr/LbvaNySK1/BTy4cBOCjhCNY2raWBwK4v+WR5J4= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.1 h1:5jYRvUIEI0LVOUYbI6ixev/ctOb8/0eJDFa6k39cMCk= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.40.1/go.mod h1:im0buuAzIxokGb9JH/bXAhiDxp9OElYL6jSQXTLiRcA= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.1 h1:9UiObaZsmKoR1k/dE6z/3laTkhkV0xnYXT8jIpMhuz8= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.33.1/go.mod h1:zU5eWYw3HNkPtcrFwBAdMv3+h3dFpmB0ng7z8wOuSPc= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.1 h1:3NrodkeRcnK301QWIjCV4BibPEQjefanYpQ+0qWWsKQ= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.21.1/go.mod h1:REsB292vC0/tIV3dUQniYqsXj4hwQwV7IZMl7fnbpHU= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.13 h1:zmKtGN1dMQDVBsfCePykMQmTfWY+jlaUTv55RF5b31w= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.13/go.mod h1:1UzMv5n56AjbPR9834o5YLw5dH6baIsY60Ib84s1NCc= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.12 h1:IXSDCqEfL4oe4plEt0GkjkuI9T3tbVH91udMp7ZwV20= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.12/go.mod h1:47OjVuK2ib5x+7RLlacLxhZRlTnjlXAwal1BSXwj7Tk= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13 h1:3A8vxp65nZy6aMlSCBvpIyxIbAN0DOSxaPDZuzasxuU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.13/go.mod h1:IxJ/pMQ/Y+MDFGo6pQRyqzKKwtGMHb5IWp5PXSQr8dM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.11 h1:QNkz5KqOUdeq1D0AP9r7Af6hNKyb0fnFa/L4DEKTp+Q= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.11/go.mod h1:c7R1eDLOU5hQ4f66TYzyAT2AeLLtw5khZJpbGCo1cYU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.56.0 h1:NZIFz15bhrWwewGU0tdUGsisKPQxvzy3O4dL5jgBDKw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.56.0/go.mod h1:ha/DkVoeDtS0XwRKyOiXP2J4Vzo3zpiE0yGi7Ej0X3o= -github.com/aws/aws-sdk-go-v2/service/sso v1.21.0 h1:P0zUA+5liaoNILI/btBBQHC09PFPyRJr+w+Xt9KHKck= -github.com/aws/aws-sdk-go-v2/service/sso v1.21.0/go.mod h1:0bmRzdsq9/iNyP02H4UV0ZRjFx6qQBqRvfCJ4trFgjE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0 h1:jPV8U9r3msO9ECm9geW8PGjU/rz8vfPTPmIBbA83W3M= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.0/go.mod h1:B3G77bQDCmhp0RV0P/J9Kd4/qsymdWVhzTe3btAtywE= -github.com/aws/aws-sdk-go-v2/service/sts v1.29.0 h1:dqW4XRwPE/poWSqVntpeXLHzpPK6AOfKmL9QWDYl9aw= -github.com/aws/aws-sdk-go-v2/service/sts v1.29.0/go.mod h1:j8+hrxlmLR8ZQo6ytTAls/JFrt5bVisuS6PD8gw2VBw= -github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.0 h1:vcy97DbO72iIUHLhoeGUSwQBgwESRz0/RE6ori2qRPE= -github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.0/go.mod h1:KKWkumFpy/2QsK0oMNLVNhTK8Hj30DNeQ2kedEsW6u8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14 h1:oWccitSnByVU74rQRHac4gLfDqjB6Z1YQGOY/dXKedI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.14/go.mod h1:8SaZBlQdCLrc/2U3CEO48rYj9uR8qRsPRkmzwNM52pM= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.13 h1:TiBHJdrItjSsvfMRMNEPvu4gFqor6aghaQ5mS18i77c= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.13/go.mod h1:XN5B38yJn1XZvhyCeTzU5Ypha6+7UzVGj2w+aN0zn3k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14 h1:zSDPny/pVnkqABXYRicYuPf9z2bTqfH13HT3v6UheIk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.14/go.mod h1:3TTcI5JSzda1nw/pkVC9dhgLre0SNBFj2lYS4GctXKI= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12 h1:tzha+v1SCEBpXWEuw6B/+jm4h5z8hZbTpXz0zRZqTnw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.12/go.mod h1:n+nt2qjHGoseWeLHt1vEr6ZRCCxIN2KcNpJxBcYQSwI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1 h1:wsg9Z/vNnCmxWikfGIoOlnExtEU459cR+2d+iDJ8elo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.56.1/go.mod h1:8rDw3mVwmvIWWX/+LWY3PPIMZuwnQdJMCt0iVFVT3qw= +github.com/aws/aws-sdk-go-v2/service/sso v1.21.1 h1:sd0BsnAvLH8gsp2e3cbaIr+9D7T1xugueQ7V/zUAsS4= +github.com/aws/aws-sdk-go-v2/service/sso v1.21.1/go.mod h1:lcQG/MmxydijbeTOp04hIuJwXGWPZGI3bwdFDGRTv14= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1 h1:1uEFNNskK/I1KoZ9Q8wJxMz5V9jyBlsiaNrM7vA3YUQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.25.1/go.mod h1:z0P8K+cBIsFXUr5rzo/psUeJ20XjPN0+Nn8067Nd+E4= +github.com/aws/aws-sdk-go-v2/service/sts v1.29.1 h1:myX5CxqXE0QMZNja6FA1/FSE3Vu1rVmeUmpJMMzeZg0= +github.com/aws/aws-sdk-go-v2/service/sts v1.29.1/go.mod h1:N2mQiucsO0VwK9CYuS4/c2n6Smeh1v47Rz3dWCPFLdE= +github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.1 h1:d+8Gw9BYBXV3NirQNHLOSS3jC5SAI2s83rmaPKExqiA= +github.com/aws/aws-sdk-go-v2/service/verifiedpermissions v1.16.1/go.mod h1:18uGZxP22gTNTF7s3Hytn5a38uGwKbKV/twAlF5RGrU= github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/oauth2support/jwt_token_support.go b/pkg/oauth2support/jwt_token_support.go index cc760e2..54a372a 100644 --- a/pkg/oauth2support/jwt_token_support.go +++ b/pkg/oauth2support/jwt_token_support.go @@ -2,12 +2,17 @@ package oauth2support import ( "context" + "crypto/x509" + "encoding/pem" + "errors" "fmt" - "log" "net/http" "os" "strings" + log "golang.org/x/exp/slog" + + "github.com/MicahParks/jwkset" "github.com/MicahParks/keyfunc/v3" "github.com/golang-jwt/jwt/v5" "github.com/hexa-org/policy-mapper/pkg/keysupport" @@ -16,16 +21,21 @@ import ( ) const ( - EnvOAuthJwksUrl string = "HEXA_TOKEN_JWKSURL" - EnvJwtAuth string = "HEXA_JWT_AUTH_ENABLE" - EnvJwtRealm string = "HEXA_JWT_REALM" - EnvJwtAudience string = "HEXA_JWT_AUDIENCE" - EnvJwtScope string = "HEXA_JWT_SCOPE" + EnvOAuthJwksUrl string = "HEXA_TOKEN_JWKSURL" + EnvTknPubKeyFile string = "HEXA_TKN_PUBKEYFILE" + EnvJwtAuth string = "HEXA_JWT_AUTH_ENABLE" + EnvJwtRealm string = "HEXA_JWT_REALM" + EnvJwtAudience string = "HEXA_JWT_AUDIENCE" + EnvJwtScope string = "HEXA_JWT_SCOPE" + EnvJwtKid string = "HEXA_JWT_KID" EnvOAuthClientId string = "HEXA_OAUTH_CLIENT_ID" EnvOAuthClientSecret string = "HEXA_OAUTH_CLIENT_SECRET" EnvOAuthClientScope string = "HEXA_OAUTH_CLIENT_SCOPE" EnvOAuthTokenEndpoint string = "HEXA_OAUTH_TOKEN_ENDPOINT" + + Header_Email string = "X-JWT-EMAIL" + Header_Subj string = "X-JWT-SUBJECT" ) type ResourceJwtAuthorizer struct { @@ -34,62 +44,117 @@ type ResourceJwtAuthorizer struct { enable bool Key keyfunc.Keyfunc Aud string - Scope string } -func NewResourceJwtAuthorizer() *ResourceJwtAuthorizer { +func NewResourceJwtAuthorizer() (*ResourceJwtAuthorizer, error) { enable := os.Getenv(EnvJwtAuth) if enable == "true" { - url := os.Getenv(EnvOAuthJwksUrl) - if url != "" { - jwkKeyfunc, err := keyfunc.NewDefaultCtx(context.Background(), []string{url}) + jwksUrl := os.Getenv(EnvOAuthJwksUrl) + keyPath := os.Getenv(EnvTknPubKeyFile) + + if jwksUrl == "" && keyPath == "" { + return nil, errors.New(fmt.Sprintf("One of %s or %s environment variables must be set to validate authorizations", EnvOAuthTokenEndpoint, EnvTknPubKeyFile)) + } + + realm := os.Getenv(EnvJwtRealm) + if realm == "" { + log.Warn(fmt.Sprintf("Warning: realm environment value not set (%s)", EnvJwtRealm)) + realm = "UNDEFINED" + } + aud := os.Getenv(EnvJwtAudience) + if aud == "" { + log.Warn(fmt.Sprintf("Warning: audience environment value not set (%s)", EnvJwtAudience)) + } + + if jwksUrl != "" { + jwkKeyfunc, err := keyfunc.NewDefaultCtx(context.Background(), []string{jwksUrl}) if err != nil { - log.Fatalf("Failed to create client JWK set. Error: %s", err) - } - realm := os.Getenv(EnvJwtRealm) - if realm == "" { - log.Println(fmt.Sprintf("Warning: realm environment value not set (%s)", EnvJwtRealm)) - realm = "UNDEFINED" - } - aud := os.Getenv(EnvJwtAudience) - if aud == "" { - log.Println(fmt.Sprintf("Warning: audience environment value not set (%s)", EnvJwtAudience)) - log.Println("Defaulting to aud=orchestrator") - aud = "orchestrator" - } - scope := os.Getenv(EnvJwtScope) - if scope == "" { - log.Println(fmt.Sprintf("Warning: scope environment value not set (%s)", EnvOAuthClientScope)) - log.Println("Defaulting to scope=orchestrator") - scope = "orchestrator" + log.Error("Failed to create client JWK set. Error: %s", err) + return nil, err } + return &ResourceJwtAuthorizer{ - jwksUrl: url, + jwksUrl: jwksUrl, enable: true, Key: jwkKeyfunc, realm: realm, Aud: aud, - Scope: scope, - } + }, nil + } + + jwkKeyfunc, err := getKeyFuncFromFile(os.Getenv(EnvJwtKid), keyPath) + if err != nil { + log.Error("Failed to load JWK set from file. Error: %s", err) + return nil, err } - log.Fatalf("Configuration parameter %s not set", EnvOAuthJwksUrl) + return &ResourceJwtAuthorizer{ + jwksUrl: jwksUrl, + enable: true, + Key: jwkKeyfunc, + realm: realm, + Aud: aud, + }, nil + + } + log.Info("JWT Authentication disabled.") + return &ResourceJwtAuthorizer{enable: false}, nil +} + +func getKeyFuncFromFile(name string, path string) (keyfunc.Keyfunc, error) { + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, errors.New(fmt.Sprintf("Unalbe to load public key (%s): %s", path, err.Error())) + } + + derBlock, _ := pem.Decode(pemBytes) + publicKey, err := x509.ParsePKCS1PublicKey(derBlock.Bytes) + if err != nil { + return nil, err } - log.Println("JWT Authentication disabled.") - return &ResourceJwtAuthorizer{enable: false} + + jwk, _ := jwkset.NewJWKFromKey(publicKey, jwkset.JWKOptions{ + Metadata: jwkset.JWKMetadataOptions{ + ALG: "RS256", + KID: name, + }, + }) + + store := jwkset.NewMemoryStorage() + _ = store.KeyWrite(context.Background(), jwk) + + options := keyfunc.Options{ + Storage: store, + Ctx: context.Background(), + } + + return keyfunc.New(options) + } -func JwtAuthenticationHandler(next http.HandlerFunc, s *ResourceJwtAuthorizer) http.HandlerFunc { +func scopeMatch(scopesAccepted []string, scopesHave []string) bool { + for _, acceptedScope := range scopesAccepted { + for _, scope := range scopesHave { + if strings.EqualFold(scope, acceptedScope) { + return true + } + + } + } + return false +} + +func JwtAuthenticationHandler(next http.HandlerFunc, s *ResourceJwtAuthorizer, scopes []string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if s.enable { if r.Header.Get("Authorization") == "" { - log.Println("Request missing authorization header") + log.Info("Request missing authorization header") w.Header().Set("www-authenticate", fmt.Sprintf("Bearer realm=\"%s\"", s.realm)) w.WriteHeader(http.StatusUnauthorized) return } - cred, valid := s.authenticate(w, r) + cred, valid := s.authenticate(w, r, scopes) if !valid { // Error has already been encoded in the response return @@ -105,12 +170,23 @@ func JwtAuthenticationHandler(next http.HandlerFunc, s *ResourceJwtAuthorizer) h } } -type AccessTokenInfo struct { +type AccessToken struct { *jwt.RegisteredClaims - Scope string `json:"scope"` + Email string `json:"email,omitempty"` + Scope string `json:"scope,omitempty"` + Roles []string `json:"roles,omitempty"` +} + +func (s *ResourceJwtAuthorizer) ValidateAuthorization(w http.ResponseWriter, r *http.Request, scopes []string) *AccessToken { + token, valid := s.authenticate(w, r, scopes) + + if !valid { + return nil + } + return token.Claims.(*AccessToken) } -func (s *ResourceJwtAuthorizer) authenticate(w http.ResponseWriter, r *http.Request) (*jwt.Token, bool) { +func (s *ResourceJwtAuthorizer) authenticate(w http.ResponseWriter, r *http.Request, scopeAccepted []string) (*jwt.Token, bool) { authorization := r.Header.Get("Authorization") if authorization == "" { w.Header().Set("www-authenticate", fmt.Sprintf("Bearer realm=\"%s\"", s.realm)) @@ -129,7 +205,7 @@ func (s *ResourceJwtAuthorizer) authenticate(w http.ResponseWriter, r *http.Requ if strings.EqualFold(parts[0], "bearer") { tokenString := strings.TrimSpace(parts[1]) - token, err := jwt.ParseWithClaims(tokenString, &AccessTokenInfo{}, s.Key.Keyfunc) + token, err := jwt.ParseWithClaims(tokenString, &AccessToken{}, s.Key.Keyfunc) if err != nil { headerMsg := fmt.Sprintf("Bearer realm=\"%s\", error=\"invalid_token\", error_description=\"%s\"", s.realm, err.Error()) @@ -139,41 +215,53 @@ func (s *ResourceJwtAuthorizer) authenticate(w http.ResponseWriter, r *http.Requ return nil, false } - // Check Audience - audMatch := false - var audStrings []string - audStrings, err = token.Claims.GetAudience() - if err != nil { - log.Printf("Error parsing audience from token claims: %s", err.Error()) + if claims, ok := token.Claims.(*AccessToken); ok { + r.Header.Set(Header_Subj, claims.Subject) + r.Header.Set(Header_Email, claims.Email) } - for _, aud := range audStrings { - if strings.EqualFold(aud, s.Aud) { - audMatch = true + + // Check Audience + if s.Aud != "" { + audMatch := false + var audStrings []string + audStrings, err = token.Claims.GetAudience() + if err != nil { + log.Info("Error parsing audience from token claims: %s", err.Error()) + } + for _, aud := range audStrings { + if strings.EqualFold(aud, s.Aud) { + audMatch = true + } + } + if !audMatch { + headerMsg := fmt.Sprintf("Bearer realm=\"%s\", error=\"invalid_token\", error_description=\"invalid audience\"", s.realm) + w.Header().Set("www-authenticate", headerMsg) + w.WriteHeader(http.StatusUnauthorized) + // log.Printf("Authorization invalid: [%s]\n", err.Error()) + return nil, false } - } - if !audMatch { - headerMsg := fmt.Sprintf("Bearer realm=\"%s\", error=\"invalid_token\", error_description=\"invalid audience\"", s.realm) - w.Header().Set("www-authenticate", headerMsg) - w.WriteHeader(http.StatusUnauthorized) - // log.Printf("Authorization invalid: [%s]\n", err.Error()) - return nil, false } - scopeMatch := false var scopes []string - atToken := token.Claims.(*AccessTokenInfo) + atToken := token.Claims.(*AccessToken) scopeString := atToken.Scope scopes = strings.Split(scopeString, " ") - if s.Scope != "" { - for _, scope := range scopes { - if strings.EqualFold(s.Scope, scope) { - scopeMatch = true - } + sMatch := true + if scopeAccepted != nil { + sMatch = scopeMatch(scopeAccepted, scopes) + } + + // check roles + if !sMatch { + tokenRoles := atToken.Roles + if tokenRoles != nil && len(tokenRoles) > 0 { + sMatch = scopeMatch(scopeAccepted, tokenRoles) } } - if !scopeMatch { - headerMsg := fmt.Sprintf("Bearer realm=\"%s\", error=\"insufficient_scope\", error_description=\"requires scope=%s\"", s.realm, s.Scope) + if !sMatch { + scopesRequired := strings.Join(scopeAccepted, ",") + headerMsg := fmt.Sprintf("Bearer realm=\"%s\", error=\"insufficient_scope\", error_description=\"requires scope=%s\"", s.realm, scopesRequired) w.Header().Set("www-authenticate", headerMsg) w.WriteHeader(http.StatusForbidden) // log.Printf("Authorization invalid: [%s]\n", err.Error()) @@ -213,7 +301,7 @@ func NewJwtClientHandler() JwtClientHandler { secret := os.Getenv(EnvOAuthClientSecret) tokenUrl := os.Getenv(EnvOAuthTokenEndpoint) if tokenUrl == "" { - log.Println(fmt.Sprintf("Error: Token endpoint (%s) not declared", EnvOAuthTokenEndpoint)) + log.Error(fmt.Sprintf("Error: Token endpoint (%s) not declared", EnvOAuthTokenEndpoint)) } config := &clientcredentials.Config{ diff --git a/pkg/oauth2support/jwt_token_test.go b/pkg/oauth2support/jwt_token_test.go index ec2750b..8146efa 100644 --- a/pkg/oauth2support/jwt_token_test.go +++ b/pkg/oauth2support/jwt_token_test.go @@ -9,6 +9,8 @@ import ( "net/http/httptest" "net/url" "os" + "path/filepath" + "runtime" "testing" "time" @@ -48,13 +50,13 @@ func TestResourceServer(t *testing.T) { _ = os.Setenv(EnvJwtAuth, "true") _ = os.Setenv(EnvJwtAudience, s.audience) _ = os.Setenv(EnvJwtScope, "orchestrator") - s.jwtHandler = NewResourceJwtAuthorizer() + s.jwtHandler, _ = NewResourceJwtAuthorizer() assert.NotNil(t, s.jwtHandler) assert.Equal(t, mockUrlJwks, s.jwtHandler.jwksUrl) assert.Equal(t, "TEST_REALM", s.jwtHandler.realm) assert.NotNil(t, s.jwtHandler.Key) - helloHandler := JwtAuthenticationHandler(oidctestsupport.HandleHello, s.jwtHandler) + helloHandler := JwtAuthenticationHandler(oidctestsupport.HandleHello, s.jwtHandler, nil) assert.NoError(t, err, "Should be a valid url") @@ -99,7 +101,7 @@ func (s *testData) Test1_JWT() { req := httptest.NewRequest("GET", "/hello", nil) req.Header.Set("Authorization", "Bearer "+tokenString) - token, valid := s.jwtHandler.authenticate(httptest.NewRecorder(), req) + token, valid := s.jwtHandler.authenticate(httptest.NewRecorder(), req, []string{"orchestrator"}) assert.True(s.T(), valid, "Token was valid") sub, _ := token.Claims.GetSubject() @@ -115,7 +117,7 @@ func (s *testData) Test2_JWT_Errors() { req.Header.Set("Authorization", tokenString) resp := httptest.NewRecorder() - token, valid := s.jwtHandler.authenticate(resp, req) + token, valid := s.jwtHandler.authenticate(resp, req, []string{"orchestrator"}) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) @@ -128,7 +130,7 @@ func (s *testData) Test2_JWT_Errors() { req2 := httptest.NewRequest("GET", "/hello", nil) req2.Header.Set("Authorization", tokenString) resp2 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp2, req2) + token, valid = s.jwtHandler.authenticate(resp2, req2, []string{"orchestrator"}) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp2.Code) @@ -140,7 +142,7 @@ func (s *testData) Test2_JWT_Errors() { fmt.Println("Missing Authorization") req3 := httptest.NewRequest("GET", "/hello", nil) resp3 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp3, req3) + token, valid = s.jwtHandler.authenticate(resp3, req3, nil) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp3.Code) @@ -154,7 +156,7 @@ func (s *testData) Test2_JWT_Errors() { req4 := httptest.NewRequest("GET", "/hello", nil) req4.Header.Set("Authorization", "Bearer Bearer "+tokenString) resp4 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp4, req4) + token, valid = s.jwtHandler.authenticate(resp4, req4, []string{"orchestrator"}) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp4.Code) @@ -168,7 +170,7 @@ func (s *testData) Test2_JWT_Errors() { req5 := httptest.NewRequest("GET", "/hello", nil) req5.Header.Set("Authorization", "Bearer "+expireTokenString) resp5 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp5, req5) + token, valid = s.jwtHandler.authenticate(resp5, req5, nil) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp4.Code) @@ -182,7 +184,7 @@ func (s *testData) Test2_JWT_Errors() { req6 := httptest.NewRequest("GET", "/hello", nil) req6.Header.Set("Authorization", "Bearer "+tokenString) resp6 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp6, req6) + token, valid = s.jwtHandler.authenticate(resp6, req6, nil) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusUnauthorized, resp6.Code) @@ -195,7 +197,7 @@ func (s *testData) Test2_JWT_Errors() { req7 := httptest.NewRequest("GET", "/hello", nil) req7.Header.Set("Authorization", "Bearer "+tokenString) resp7 := httptest.NewRecorder() - token, valid = s.jwtHandler.authenticate(resp7, req7) + token, valid = s.jwtHandler.authenticate(resp7, req7, []string{"orchestrator"}) assert.False(s.T(), valid) assert.Nil(s.T(), token) assert.Equal(s.T(), http.StatusForbidden, resp7.Code) @@ -221,7 +223,7 @@ func (s *testData) Test3_JwtHandlerToken() { req := httptest.NewRequest("GET", "/hello", nil) req.Header.Set("Authorization", "Bearer "+tokenString) - tokenParsed, valid := s.jwtHandler.authenticate(httptest.NewRecorder(), req) + tokenParsed, valid := s.jwtHandler.authenticate(httptest.NewRecorder(), req, nil) assert.True(s.T(), valid, "Token was valid") sub, _ := tokenParsed.Claims.GetSubject() @@ -302,3 +304,23 @@ func (s *testData) Test5_Middleware_Error() { wwwAuthHeader = resp.Header.Get("WWW-Authenticate") assert.Equal(s.T(), "Bearer realm=\"TEST_REALM\", error=\"invalid_token\", error_description=\"token is malformed: token contains an invalid number of segments\"", wwwAuthHeader) } + +func (s *testData) Test6_LoadPubKeyFile() { + _, file, _, _ := runtime.Caller(0) + keyfile := filepath.Join(file, "../test/issuer-cert.pem") + + _ = os.Unsetenv(EnvOAuthJwksUrl) + _ = os.Setenv(EnvTknPubKeyFile, keyfile) + authorizer, err := NewResourceJwtAuthorizer() + assert.NoError(s.T(), err) + assert.NotNil(s.T(), authorizer) + assert.NotNil(s.T(), authorizer.Key) + + // Test wrong certificate type (in this case TLS cert) + keyfile = filepath.Join(file, "../test/ca-cert.pem") + _ = os.Setenv(EnvTknPubKeyFile, keyfile) + authorizer, err = NewResourceJwtAuthorizer() + assert.Error(s.T(), err, "asn1: structure error: tags don't match (2 vs {class:0 tag:16 length:853 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:0 set:false omitEmpty:false} @4") + assert.Nil(s.T(), authorizer) + +} diff --git a/pkg/oauth2support/test/ca-cert.pem b/pkg/oauth2support/test/ca-cert.pem new file mode 100644 index 0000000..bf96ec4 --- /dev/null +++ b/pkg/oauth2support/test/ca-cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbTCCA1WgAwIBAgICB+MwDQYJKoZIhvcNAQELBQAwSDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNPMRAwDgYDVQQHEwdCb3VsZGVyMRowGAYDVQQKExFIZXhhIE9y +Z2FuaXphdGlvbjAeFw0yNDA2MTIxODIwMjNaFw0zNDA2MTIxODIwMjNaMEgxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIEwJDTzEQMA4GA1UEBxMHQm91bGRlcjEaMBgGA1UE +ChMRSGV4YSBPcmdhbml6YXRpb24wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDtn1lTwqFtx+bwA/5VtcHAmZx3+cyDufcK57f5Ovn0eiNmfu+8t5Y403OP +CISYbKMYUCJlaOdZpDLwxSDqEG2m7kXlF8eiz92kMeTuvBnivADlUGzbliSi7MH8 +kvoZNbXssYnqiJdcgn+kkPbijTN2r6DmngHO4GaHIeQGEY4fJeLt6ejR49t0Um9q +ISi/FkfO8/uWzqTIxXhDYnJwUJ916MZk0s0K9acV16Tk/yRYc0QRkcy1QcmSSMM0 +/Ii0+TVRCUE88lJduSSXTUofCpo9ACrcSIRC4WmFbp/X7NHU7VpckTKTIwF6Imi4 +FFMII/Jlb2oLjanfKyJ1aDJYLhj2Jaf7Zlbir+ejlApqprbbpLERnZm690SPKOV4 +PT8CRTpdCIOOsRnYaeOX1MMe2D56XyiOHX8dzDWFJrTIrDmjieB04XIAo35xOMet +xP1fddvFgfRdXB/912fkQ7tVEFjL+M/s7trfqNO500eeJZr6LNLElnIYMo1SFtXI +onWUrNhJrwTAS4EamBtHPl2uYA45QL5xpnsRznWnCYGoxHNSD/9upBhjpfrKJ8Hd +gS/tVvGt17NkT8Cm3szu9sPbG2lBiF85BN6sjvBZN6vTqqpvW+2wrqgvvZ3HdpFC +83mnqwXDm+rH2UzA7wC/BFZusPI4s051dhcClnV33EZ9cBUgWQIDAQABo2EwXzAO +BgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFGmqt9Xe8wdWZ46pOu6YxZBgRNxtMA0G +CSqGSIb3DQEBCwUAA4ICAQA0i/qCm1QRKciqh5gjjolzLEN/JigdoIEsOwiRARWq +uZ761RzTSpRHbYKvl0RxAwkdc3rihSl5YeFSfv2D968+1YAORLFZtLMcRcTUyFbb +PvlupPWzVibJy5PqdqDma5ylbY6iNrqBsaY3FwYLwS7J7Nw2DRDWbllspa5toYLX +d7lSf3AGWAPXkWtP7V+eDB3VWaznWdQEEWQJ6sCMF/CCZMZ5uu0JNQzqKAYC0Fpe +EZ0iBpVsm0MZ2I0rzhkbZQdStLTG/lRBS+UEte5ZV5SLizSnowebt+we4Q8Rk+VF +z8/k5lq0IywJn/L3sbvLnibGEdEZTkjEw9erCNus9MZtZ5Mzxtyy9tC3AAU6Ybvt +3oGfk378zlGXHR6Asn1xH697mlOJPTUNuBbDvfOi5SWktYJWq3AFD0z0mkdVNkB/ +Oq5hxvZMxVJshtpd4HxNAI6h2yujk4MSA0wIYWsnDuFAed7lHfUjQ1JO5s4b/Hbb +P6c+/LtR2dqf7DrcBiOGlNhN2bU0EMzT3MrK3beGcHIuie/kDeTqey9/hdKXQd6Y +Oyd5nD/FsQrA0EKDXGaC7up/eM4oea3zsVQRfzSEpXuZFzxGoeX1sG/BbrE5NKPt +3HCgt6lQCdg2gqPmlYK8e/ynEmohGeN90gw39mYqJXe/E1+BWUk4gMurMRgKu15y +mw== +-----END CERTIFICATE----- diff --git a/pkg/oauth2support/test/issuer-cert.pem b/pkg/oauth2support/test/issuer-cert.pem new file mode 100644 index 0000000..bdf42fd --- /dev/null +++ b/pkg/oauth2support/test/issuer-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCgKCAgEAvNRFthciXE/Etlxb6XkhID4Zcz7u2Xz9YrbRFBaA6j8FvglXs9GQ +ioLqwOzTDjDndFrjeB7M68Pudun3yvEMfLmktiTJvYRcwl3wOl45Op4S3ciKgAXZ +VTJYxBmltxeZUd86lz563H0nficBnI1C7/S/qPaHU8F0JkhSH+EBYFCtOdTVh8X9 +8tqcTuUErWMR+g0TshBbdqKqxEOqLTd3BvnPHOxbVICv2CF0miI6g/ObRxYswMZ1 +Na7uTPhqtlVLvLKWbalQt5HhJdw8TojtiPbRULL2fs//Q4pi68BF1xAow2GiGyeo +2P8pSKxnzi8ZPFfJlCInBRyI/2vse+yGYIKTVV9up+kW9oCB0zfRvXbVz3Us0NPQ +735bTkFLdQB2DA2IsI0rmtn6warmOxv++uepRnn7gxBM4huqWl5kZl595ht7VYUg +lLT0PEqvuOs8vqbl2GsO/xun2op9uX4SgY8DuFZrndygG6ZeDmOGFB2fMYhMWx2F +4uqTtfplojQL4nasDla2ScBWRVlV2BGEpp8IYIDQf3Vsw8KgFCqhMZRquwoMxO99 +wHwxkbUPCCEv8unaivZfGT6UwVcyNNMhpLjWVoCf/8UnQklBuFJU41Slxv+fkq3x +0t5GVD6qqYPVMY+U3G8xkuE6If4KHzmHpUSOx1y+mpmaZV03nNxmaAUCAwEAAQ== +-----END CERTIFICATE----- diff --git a/pkg/tokensupport/tokenGenerator.go b/pkg/tokensupport/tokenGenerator.go new file mode 100644 index 0000000..4319413 --- /dev/null +++ b/pkg/tokensupport/tokenGenerator.go @@ -0,0 +1,329 @@ +package tokensupport + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/MicahParks/jwkset" + "github.com/MicahParks/keyfunc/v3" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/hexa-org/policy-mapper/pkg/oauth2support" +) + +const ( + ScopeBundle string = "bundle" + ScopeDecision string = "az" + ScopeAdmin string = "root" + EnvTknKeyDirectory string = "HEXA_TKN_DIRECTORY" + EnvTknPrivateKeyFile string = "HEXA_TKN_PRIVKEYFILE" + EnvTknPubKeyFile string = "HEXA_TKN_PUBKEYFILE" + EnvTknJwksUrl string = "HEXA_TKN_JWKS_URL" + + DefTknPrivateKeyFile string = "issuer-priv.pem" + DefTknPublicKeyFile string = "issuer-cert.pem" + EnvTknEnforceMode string = "HEXA_TKN_MODE" + EnvTknIssuer string = "HEXA_TKN_ISSUER" + + ModeEnforceAnonymous = "ANON" + ModeEnforceBundle = "BUNDLE" + ModeEnforceAll = "ALL" +) + +type JwtAuthToken struct { + Scopes []string `json:"roles,omitempty"` + Email string `json:"email,omitempty"` + jwt.RegisteredClaims +} + +type TokenHandler struct { + TokenIssuer string + PrivateKey *rsa.PrivateKey + PublicKey keyfunc.Keyfunc + Authorizer *oauth2support.ResourceJwtAuthorizer + KeyDir string + PrivateKeyPath string + PublicKeyPath string + Mode string +} + +func (a *TokenHandler) PrivateKeyExists() bool { + stat, err := os.Stat(a.PrivateKeyPath) + return err == nil && !stat.IsDir() +} + +func getConfig() *TokenHandler { + validationString := os.Getenv(EnvTknEnforceMode) + var validationMode string + + switch strings.ToUpper(validationString) { + case ModeEnforceAll: + validationMode = ModeEnforceAll + case ModeEnforceBundle: + validationMode = ModeEnforceBundle + case ModeEnforceAnonymous: + validationMode = ModeEnforceAnonymous + default: + validationMode = ModeEnforceAll + } + + privateKeyPath := os.Getenv(EnvTknPrivateKeyFile) + publicKeyPath := os.Getenv(EnvTknPubKeyFile) + keyDir := os.Getenv(EnvTknKeyDirectory) + if keyDir == "" { + if privateKeyPath == "" { + if publicKeyPath == "" { + // Default everything + home := os.Getenv("HOME") + fmt.Println(fmt.Sprintf("HOME=[%s]", home)) + keyDir = filepath.Join(home, "./.certs") + fmt.Println(fmt.Sprintf("Setting default key directory of: %s", keyDir)) + } else { + // This is likely just a validator + keyDir = filepath.Dir(publicKeyPath) + } + } else { + // This is likely an issuer + keyDir = filepath.Dir(privateKeyPath) + } + } + + fmt.Println("Using key directory: " + keyDir) + err := os.MkdirAll(keyDir, 0755) + if err != nil { + panic(fmt.Sprintf("Was unable to open or create certificate directory(%s):%s", keyDir, err)) + } + + if privateKeyPath == "" { + privateKeyPath = filepath.Join(keyDir, DefTknPrivateKeyFile) + } + + if publicKeyPath == "" { + publicKeyPath = filepath.Join(keyDir, DefTknPublicKeyFile) + } + return &TokenHandler{ + KeyDir: keyDir, + PublicKeyPath: publicKeyPath, + PrivateKeyPath: privateKeyPath, + Mode: validationMode, + } + +} + +func convertJWKS(name string, pubKey *rsa.PublicKey) keyfunc.Keyfunc { + + jwk, _ := jwkset.NewJWKFromKey(pubKey, jwkset.JWKOptions{ + Metadata: jwkset.JWKMetadataOptions{ + ALG: "RS256", + KID: name, + }, + }) + + store := jwkset.NewMemoryStorage() + _ = store.KeyWrite(context.Background(), jwk) + + options := keyfunc.Options{ + Storage: store, + Ctx: context.Background(), + } + + jwks, _ := keyfunc.New(options) + + return jwks + +} + +func (a *TokenHandler) loadIssuer(name string) error { + pemBytes, err := os.ReadFile(a.PrivateKeyPath) + if err != nil { + return err + } + + derBlock, _ := pem.Decode(pemBytes) + + privateKey, err := x509.ParsePKCS1PrivateKey(derBlock.Bytes) + if err != nil { + return err + } + a.TokenIssuer = name + a.PrivateKey = privateKey + a.PublicKey = convertJWKS(name, &privateKey.PublicKey) + + return nil +} + +func LoadIssuer(name string) (*TokenHandler, error) { + handler := getConfig() + return handler, handler.loadIssuer(name) +} + +/* +GenerateIssuerKeys will create a new JWT issuer private and public key set. Set keepExisting to +true to enable auto-generation on first execution. +*/ +func GenerateIssuerKeys(name string, keepExisting bool) (*TokenHandler, error) { + handler := getConfig() + + if handler.PrivateKeyExists() && keepExisting { + // This enables docker services to auto generate on first execution. + return handler, handler.loadIssuer(name) + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + fmt.Println("Unexpected crypto error generating keys: " + err.Error()) + os.Exit(-1) + } + privateKeyPEM := new(bytes.Buffer) + _ = pem.Encode(privateKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + publicKey := privateKey.PublicKey + + pubKeyBytes := x509.MarshalPKCS1PublicKey(&publicKey) + pubKeyPEM := new(bytes.Buffer) + _ = pem.Encode(pubKeyPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: pubKeyBytes, + }) + + err = os.WriteFile(handler.PrivateKeyPath, privateKeyPEM.Bytes(), 0644) + if err != nil { + fmt.Printf("Error writing key file: %s", err.Error()) + return nil, err + } + + pubKeyFile := filepath.Join(handler.KeyDir, DefTknPublicKeyFile) + err = os.WriteFile(pubKeyFile, pubKeyPEM.Bytes(), 0644) + + handler.TokenIssuer = name + handler.PrivateKey = privateKey + handler.PublicKey = convertJWKS(name, &publicKey) + return handler, nil +} + +func (a *TokenHandler) IssueToken(scopes []string, email string) (string, error) { + if a.PrivateKey == nil { + return "", errors.New("validation mode only") + } + exp := time.Now().AddDate(0, 6, 0) + tokenInfo := JwtAuthToken{ + Scopes: scopes, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(exp), + Audience: []string{a.TokenIssuer}, + Issuer: a.TokenIssuer, + ID: uuid.New().String(), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenInfo) + token.Header["typ"] = "jwt" + token.Header["kid"] = a.TokenIssuer + return token.SignedString(a.PrivateKey) +} + +/* +// ValidateAuthorization evaluates the authorization header and checks to see if the correct scope is asserted. +// 200 OK means authorized. Forbidden returned if wrong scope, otherwise unauthorized +func (a *TokenHandler) ValidateAuthorization(r *http.Request, scopes []string) (*JwtAuthToken, int) { + switch a.Mode { + case ModeEnforceAnonymous: + return nil, http.StatusOK // Anonymous mode should only be used for testing! + case ModeEnforceBundle: + // If the request is for a decision, and we are enforcing bundle only, return success + if scopeMatch(scopes, []string{ScopeDecision}) { + return nil, http.StatusOK // Decisions can proceed without authorization + } + default: + // continue enforcement. + } + + authorization := r.Header.Get("Authorization") + if authorization == "" { + return nil, http.StatusUnauthorized + } + + parts := strings.Split(authorization, " ") + if len(parts) < 2 { + return nil, http.StatusUnauthorized + } + if strings.EqualFold(parts[0], "bearer") { + + tkn, err := a.ParseAuthToken(parts[1]) + if err != nil { + log.Printf("Authorization invalid: [%s]\n", err.Error()) + return nil, http.StatusUnauthorized + } + + if tkn.IsScopeMatch(scopes) { + return tkn, http.StatusOK + } + return nil, http.StatusForbidden + } + log.Printf("Received invalid authorization: %s\n", parts[0]) + return nil, http.StatusUnauthorized +} + +// ParseAuthToken parses and validates an authorization token. An *JwtAuthToken is only returned if the token was validated otherwise nil +func (a *TokenHandler) ParseAuthToken(tokenString string) (*JwtAuthToken, error) { + if a.PublicKey == nil { + return nil, errors.New("no public key provided to validate authorization token") + } + + // In case of cut/paste error, trim extra spaces + tokenString = strings.TrimSpace(tokenString) + + valid := true + + token, err := jwt.ParseWithClaims(tokenString, &JwtAuthToken{}, a.PublicKey.Keyfunc) + if err != nil { + log.Printf("Error validating token: %s", err.Error()) + valid = false + } + if token == nil || token.Header["typ"] != "jwt" { + log.Printf("token is not an authorization token (JWT)") + return nil, errors.New("token type is not an authorization token (`jwt`)") + } + + if claims, ok := token.Claims.(*JwtAuthToken); ok && valid { + return claims, nil + } + + return nil, err +} + + +func scopeMatch(scopesAccepted []string, scopesHave []string) bool { + for _, acceptedScope := range scopesAccepted { + for _, scope := range scopesHave { + if strings.EqualFold(scope, ScopeAdmin) { + return true + } + if strings.EqualFold(scope, acceptedScope) { + return true + } + + } + } + return false +} + +func (t *JwtAuthToken) IsScopeMatch(scopesAccepted []string) bool { + return scopeMatch(scopesAccepted, t.Scopes) +} + +*/ diff --git a/pkg/tokensupport/tokenGenerator_test.go b/pkg/tokensupport/tokenGenerator_test.go new file mode 100644 index 0000000..59d96e1 --- /dev/null +++ b/pkg/tokensupport/tokenGenerator_test.go @@ -0,0 +1,187 @@ +package tokensupport + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/hexa-org/policy-mapper/pkg/oauth2support" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type testSuite struct { + suite.Suite + keyDir string + keyfile string + Handler *TokenHandler + bundleToken string + azToken string +} + +func TestTokenGenerator(t *testing.T) { + path, _ := os.MkdirTemp("", "token-*") + + _ = os.Setenv(EnvTknKeyDirectory, path) + _ = os.Unsetenv(EnvTknPubKeyFile) + _ = os.Unsetenv(EnvTknPrivateKeyFile) + + handler, err := GenerateIssuerKeys("authzen", false) + assert.NoError(t, err, "Check no error generating issuer") + assert.Equal(t, "authzen", handler.TokenIssuer, "Check issuer set") + + s := testSuite{ + Suite: suite.Suite{}, + keyDir: path, + keyfile: handler.PrivateKeyPath, + Handler: handler, + } + _ = os.Setenv(EnvTknPubKeyFile, filepath.Join(s.keyDir, DefTknPublicKeyFile)) + _ = os.Setenv(oauth2support.EnvJwtAuth, "true") + _ = os.Setenv(EnvTknIssuer, s.Handler.TokenIssuer) + _ = os.Setenv(oauth2support.EnvJwtKid, s.Handler.TokenIssuer) + _ = os.Unsetenv(oauth2support.EnvJwtAudience) + + suite.Run(t, &s) + + s.cleanup() +} + +func (s *testSuite) cleanup() { + _ = os.RemoveAll(s.keyDir) +} + +func (s *testSuite) TestGenerateIssuer() { + assert.Equal(s.T(), s.keyDir, filepath.Clean(s.Handler.KeyDir), "Check key directory") + assert.NotNil(s.T(), s.Handler.PublicKey, "Public key created") + dir, err := os.ReadDir(s.keyDir) + assert.NoError(s.T(), err, "able to read key dir") + numFiles := len(dir) + assert.Greater(s.T(), numFiles, 1, "should be at least 2 files") +} + +func (s *testSuite) TestLoadExisting() { + + handler2, err := LoadIssuer("authzen") + assert.NoError(s.T(), err, "No error on load") + assert.NotNil(s.T(), handler2.PrivateKey, "Check private key loaded") +} + +func (s *testSuite) TestIssueAndValidateToken() { + fmt.Println("Loading validator...") + _ = os.Unsetenv(EnvTknKeyDirectory) + _ = os.Unsetenv(EnvTknPrivateKeyFile) + + validator, err := oauth2support.NewResourceJwtAuthorizer() + + assert.NoError(s.T(), err, "No error on load") + assert.NotNil(s.T(), validator, "Check validator not null") + assert.NotNil(s.T(), validator.Key, "Keyfunc should not be nil") + // assert.Equal(s.T(), ModeEnforceAll, validator.Mode, "Check mode is enforce ALL by default") + + fmt.Println("Issuing token...") + + tokenString, err := s.Handler.IssueToken([]string{ScopeBundle}, "test@example.com") + assert.NoError(s.T(), err, "No error issuing token") + assert.NotEmpty(s.T(), tokenString, "Token has a value") + s.bundleToken = tokenString + + tokenString, err = s.Handler.IssueToken([]string{ScopeDecision}, "test@example.com") + assert.NoError(s.T(), err, "No error issuing token") + assert.NotEmpty(s.T(), tokenString, "Token has a value") + fmt.Println("Token issued:\n" + tokenString) + s.azToken = tokenString // save for the next test + + req, _ := http.NewRequest("GET", "example.com", nil) + req.Header.Set("Authorization", "Bearer "+tokenString) + + fmt.Println("Validate token...") + + rr := httptest.NewRecorder() + fmt.Println(" Positive check") + jwt := validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusOK, rr.Code, "Check status ok") + email := jwt.Email + assert.Equal(s.T(), "test@example.com", email, "Check email parsed") + + fmt.Println(" Negative checks") + + // Token should be valid but wrong scope + rr = httptest.NewRecorder() + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeBundle}) + + assert.Equal(s.T(), http.StatusForbidden, rr.Code, "Check forbidden") + + rr = httptest.NewRecorder() + // Token not valid + req.Header.Del("Authorization") + req.Header.Set("Authorization", "Bearer bleh"+tokenString) + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusUnauthorized, rr.Code, "Check unauthorized") + assert.Nil(s.T(), jwt, "JWT should be nil") + + // no authorization + rr = httptest.NewRecorder() + req.Header.Del("Authorization") + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusUnauthorized, rr.Code, "Check unauthorized") + assert.Nil(s.T(), jwt, "JWT should be nil") + + // No authorization type + rr = httptest.NewRecorder() + req.Header.Set("Authorization", tokenString) + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusUnauthorized, rr.Code, "Check unauthorized") + assert.Nil(s.T(), jwt, "JWT should be nil") +} + +func (s *testSuite) TestValidateMode() { + fmt.Println("Loading validator...") + _ = os.Unsetenv(EnvTknKeyDirectory) + _ = os.Unsetenv(EnvTknPrivateKeyFile) + // _ = os.Setenv(oauth2support.EnvJwtAuth,"false") + _ = os.Setenv(EnvTknEnforceMode, ModeEnforceBundle) + + validator, err := oauth2support.NewResourceJwtAuthorizer() + assert.NoError(s.T(), err, "No error on load") + assert.NotNil(s.T(), validator, "Check validator not null") + // assert.Equal(s.T(), ModeEnforceBundle, validator.Mode, "Check mode is enforce BUNDLE") + + fmt.Println("Validate token...") + + fmt.Println(" Positive check") + + fmt.Println(" Anonymous") + rr := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "example.com", nil) + jwt := validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusUnauthorized, rr.Code, "Check status unauthorized") + assert.Nil(s.T(), jwt, "JWT should be nil") + + fmt.Println(" Az scope token") + rr = httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+s.azToken) + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeDecision}) + assert.Equal(s.T(), http.StatusOK, rr.Code, "Check status ok") + assert.NotNil(s.T(), jwt, "JWT is not nil") + + fmt.Println(" Bundle token") + rr = httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+s.bundleToken) + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeBundle}) + assert.Equal(s.T(), http.StatusOK, rr.Code, "Check status ok") + email := jwt.Email + assert.Equal(s.T(), "test@example.com", email, "Check email parsed") + + fmt.Println(" Negative checks") + + // Token should be valid but wrong scope + rr = httptest.NewRecorder() + req.Header.Set("Authorization", "Bearer "+s.azToken) + jwt = validator.ValidateAuthorization(rr, req, []string{ScopeBundle}) + assert.Equal(s.T(), http.StatusForbidden, rr.Code, "Check forbidden") + +}