diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cfa743 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +max_line_length = 120 +quote_type = "single" + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 26fd1d2..7aa643e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ composer.phar composer.lock vendor -.DS_Store \ No newline at end of file +.DS_Store +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 76458db..0c69114 100644 --- a/README.md +++ b/README.md @@ -95,10 +95,16 @@ return [ 'destination' => 'https://example.com/saml/acs', // Simple Logout URL of the Service Provider 'logout' => 'https://example.com/saml/sls', - ] + // SP certificate + // 'certificate' => '', + // Turn off auto appending of the idp query param + // 'query_params' => false, + // Turn off the encryption of the assertion per SP + // 'encrypt_assertion' => false + ], ], // List of guards saml idp will catch Authenticated, Login and Logout events (thanks @abublihi) - 'guards' => ['web'] + 'guards' => ['web'], ]; ``` @@ -133,7 +139,6 @@ return [ 'sp_slo_redirects' => [ 'mysp.com' => 'https://mysp.com', ], - ]; ``` @@ -187,7 +192,6 @@ class SamlAssertionAttributes ->addAttribute(new Attribute(ClaimTypes::NAME, auth()->user()->name)); } } - ``` ## Digest Algorithm (optional) diff --git a/config/samlidp.php b/config/samlidp.php index eea824d..898891f 100644 --- a/config/samlidp.php +++ b/config/samlidp.php @@ -1,7 +1,6 @@ 'https://myfacebookworkplace.facebook.com/work/saml.php', // 'logout' => 'https://myfacebookworkplace.facebook.com/work/sls.php', + // // SP certificate // 'certificate' => '', - // 'query_params' => false + // // Turn off auto appending of the idp query param + // 'query_params' => false, + // // Turn off the encryption of the assertion per SP + // 'encrypt_assertion' => false // ] ], @@ -51,17 +54,11 @@ // All of the Laravel SAML IdP event / listener mappings. 'events' => [ 'CodeGreenCreative\SamlIdp\Events\Assertion' => [], - 'Illuminate\Auth\Events\Logout' => [ - 'CodeGreenCreative\SamlIdp\Listeners\SamlLogout', - ], - 'Illuminate\Auth\Events\Authenticated' => [ - 'CodeGreenCreative\SamlIdp\Listeners\SamlAuthenticated', - ], - 'Illuminate\Auth\Events\Login' => [ - 'CodeGreenCreative\SamlIdp\Listeners\SamlLogin', - ], + 'Illuminate\Auth\Events\Logout' => ['CodeGreenCreative\SamlIdp\Listeners\SamlLogout'], + 'Illuminate\Auth\Events\Authenticated' => ['CodeGreenCreative\SamlIdp\Listeners\SamlAuthenticated'], + 'Illuminate\Auth\Events\Login' => ['CodeGreenCreative\SamlIdp\Listeners\SamlLogin'], ], // List of guards saml idp will catch Authenticated, Login and Logout events - 'guards' => ['web'] + 'guards' => ['web'], ]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..40e532c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,162 @@ +{ + "name": "laravel-samlidp", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@prettier/plugin-php": "^0.18.9", + "prettier": "^2.7.1" + } + }, + "node_modules/@prettier/plugin-php": { + "version": "0.18.9", + "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.18.9.tgz", + "integrity": "sha512-d1uE9v3JsQ9uBLWlssZhO02RLI8u8jRBFPCO5ud4VbveCpSZbDRnGpSuK3IqSWHdM/OnuySz0yWr5M9/9mINvw==", + "dev": true, + "dependencies": { + "linguist-languages": "^7.21.0", + "mem": "^8.0.0", + "php-parser": "3.1.0-beta.11" + }, + "peerDependencies": { + "prettier": "^1.15.0 || ^2.0.0" + } + }, + "node_modules/linguist-languages": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.21.0.tgz", + "integrity": "sha512-KrWJJbFOvlDhjlt5OhUipVlXg+plUfRurICAyij1ZVxQcqPt/zeReb9KiUVdGUwwhS/2KS9h3TbyfYLA5MDlxQ==", + "dev": true + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/php-parser": { + "version": "3.1.0-beta.11", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.0-beta.11.tgz", + "integrity": "sha512-aKhWHXun6FKa0MX+GcJtEoLPSWuGQTiEkNgckVjT95OAnKG33c+zsDQEpXx4R74PQ030YZLNq9XV7odKapbOsg==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + }, + "dependencies": { + "@prettier/plugin-php": { + "version": "0.18.9", + "resolved": "https://registry.npmjs.org/@prettier/plugin-php/-/plugin-php-0.18.9.tgz", + "integrity": "sha512-d1uE9v3JsQ9uBLWlssZhO02RLI8u8jRBFPCO5ud4VbveCpSZbDRnGpSuK3IqSWHdM/OnuySz0yWr5M9/9mINvw==", + "dev": true, + "requires": { + "linguist-languages": "^7.21.0", + "mem": "^8.0.0", + "php-parser": "3.1.0-beta.11" + } + }, + "linguist-languages": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/linguist-languages/-/linguist-languages-7.21.0.tgz", + "integrity": "sha512-KrWJJbFOvlDhjlt5OhUipVlXg+plUfRurICAyij1ZVxQcqPt/zeReb9KiUVdGUwwhS/2KS9h3TbyfYLA5MDlxQ==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + } + }, + "mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true + }, + "php-parser": { + "version": "3.1.0-beta.11", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.1.0-beta.11.tgz", + "integrity": "sha512-aKhWHXun6FKa0MX+GcJtEoLPSWuGQTiEkNgckVjT95OAnKG33c+zsDQEpXx4R74PQ030YZLNq9XV7odKapbOsg==", + "dev": true + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b16e386 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "@prettier/plugin-php": "^0.18.9", + "prettier": "^2.7.1" + } +} diff --git a/src/Jobs/SamlSso.php b/src/Jobs/SamlSso.php index c9abc44..b702be1 100644 --- a/src/Jobs/SamlSso.php +++ b/src/Jobs/SamlSso.php @@ -2,41 +2,42 @@ namespace CodeGreenCreative\SamlIdp\Jobs; -use CodeGreenCreative\SamlIdp\Contracts\SamlContract; -use CodeGreenCreative\SamlIdp\Events\Assertion as AssertionEvent; -use CodeGreenCreative\SamlIdp\Exceptions\DestinationMissingException; -use CodeGreenCreative\SamlIdp\Traits\PerformsSingleSignOn; -use Illuminate\Foundation\Bus\Dispatchable; -use LightSaml\Binding\BindingFactory; -use LightSaml\Context\Profile\MessageContext; -use LightSaml\Credential\KeyHelper; -use LightSaml\Credential\X509Certificate; use LightSaml\Helper; -use LightSaml\Model\Assertion\Assertion; -use LightSaml\Model\Assertion\AttributeStatement; -use LightSaml\Model\Assertion\AudienceRestriction; -use LightSaml\Model\Assertion\AuthnContext; -use LightSaml\Model\Assertion\AuthnStatement; -use LightSaml\Model\Assertion\Conditions; -use LightSaml\Model\Assertion\EncryptedAssertionWriter; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use LightSaml\SamlConstants; +use LightSaml\Credential\KeyHelper; +use LightSaml\Model\Protocol\Status; +use LightSaml\Binding\BindingFactory; use LightSaml\Model\Assertion\Issuer; use LightSaml\Model\Assertion\NameID; use LightSaml\Model\Assertion\Subject; -use LightSaml\Model\Assertion\SubjectConfirmation; -use LightSaml\Model\Assertion\SubjectConfirmationData; -use LightSaml\Model\Context\DeserializationContext; -use LightSaml\Model\Protocol\AuthnRequest; use LightSaml\Model\Protocol\Response; -use LightSaml\Model\Protocol\Status; +use LightSaml\Model\Assertion\Assertion; use LightSaml\Model\Protocol\StatusCode; +use LightSaml\Credential\X509Certificate; +use LightSaml\Model\Assertion\Conditions; +use LightSaml\Model\Protocol\AuthnRequest; +use Illuminate\Foundation\Bus\Dispatchable; +use LightSaml\Model\Assertion\AuthnContext; use LightSaml\Model\XmlDSig\SignatureWriter; -use LightSaml\SamlConstants; -use Illuminate\Support\Arr; -use Illuminate\Support\Str; +use LightSaml\Context\Profile\MessageContext; +use LightSaml\Model\Assertion\AuthnStatement; +use LightSaml\Model\Assertion\AttributeStatement; +use LightSaml\Model\Assertion\AudienceRestriction; +use LightSaml\Model\Assertion\SubjectConfirmation; +use LightSaml\Model\Context\DeserializationContext; +use CodeGreenCreative\SamlIdp\Contracts\SamlContract; +use LightSaml\Model\Assertion\SubjectConfirmationData; +use LightSaml\Model\Assertion\EncryptedAssertionWriter; +use CodeGreenCreative\SamlIdp\Traits\PerformsSingleSignOn; +use CodeGreenCreative\SamlIdp\Events\Assertion as AssertionEvent; +use CodeGreenCreative\SamlIdp\Exceptions\DestinationMissingException; class SamlSso implements SamlContract { - use Dispatchable, PerformsSingleSignOn; + use Dispatchable; + use PerformsSingleSignOn; /** * [__construct description] @@ -54,10 +55,10 @@ public function __construct($guard = null) */ public function handle() { - $deserializationContext = new DeserializationContext; + $deserializationContext = new DeserializationContext(); $deserializationContext->getDocument()->loadXML(gzinflate(base64_decode(request('SAMLRequest')))); - $this->authn_request = new AuthnRequest; + $this->authn_request = new AuthnRequest(); $this->authn_request->deserialize($deserializationContext->getDocument()->firstChild, $deserializationContext); $this->setDestination(); @@ -67,24 +68,32 @@ public function handle() public function response() { - $this->response = (new Response)->setIssuer(new Issuer($this->issuer)) + $this->response = (new Response()) + ->setIssuer(new Issuer($this->issuer)) ->setStatus(new Status(new StatusCode('urn:oasis:names:tc:SAML:2.0:status:Success'))) ->setID(Helper::generateID()) - ->setIssueInstant(new \DateTime) + ->setIssueInstant(new \DateTime()) ->setDestination($this->destination) ->setInResponseTo($this->authn_request->getId()); - $assertion = new Assertion; + $assertion = new Assertion(); $assertion ->setId(Helper::generateID()) - ->setIssueInstant(new \DateTime) + ->setIssueInstant(new \DateTime()) ->setIssuer(new Issuer($this->issuer)) ->setSignature(new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm)) ->setSubject( - (new Subject) - ->setNameID((new NameID(auth($this->guard)->user()->__get(config('samlidp.email_field', 'email')), SamlConstants::NAME_ID_FORMAT_EMAIL))) + (new Subject()) + ->setNameID( + new NameID( + auth($this->guard) + ->user() + ->__get(config('samlidp.email_field', 'email')), + SamlConstants::NAME_ID_FORMAT_EMAIL + ) + ) ->addSubjectConfirmation( - (new SubjectConfirmation) + (new SubjectConfirmation()) ->setMethod(SamlConstants::CONFIRMATION_METHOD_BEARER) ->setSubjectConfirmationData( (new SubjectConfirmationData()) @@ -95,43 +104,43 @@ public function response() ) ) ->setConditions( - (new Conditions) - ->setNotBefore(new \DateTime) + (new Conditions()) + ->setNotBefore(new \DateTime()) ->setNotOnOrAfter(new \DateTime('+1 MINUTE')) - ->addItem( - new AudienceRestriction([$this->authn_request->getIssuer()->getValue()]) - ) + ->addItem(new AudienceRestriction([$this->authn_request->getIssuer()->getValue()])) ) ->addItem( - (new AuthnStatement) + (new AuthnStatement()) ->setAuthnInstant(new \DateTime('-10 MINUTE')) ->setSessionIndex(Helper::generateID()) ->setAuthnContext( - (new AuthnContext) - ->setAuthnContextClassRef(SamlConstants::NAME_ID_FORMAT_UNSPECIFIED) + (new AuthnContext())->setAuthnContextClassRef(SamlConstants::NAME_ID_FORMAT_UNSPECIFIED) ) ); - $attribute_statement = new AttributeStatement; + $attribute_statement = new AttributeStatement(); event(new AssertionEvent($attribute_statement, $this->guard)); // Add the attributes to the assertion $assertion->addItem($attribute_statement); // Encrypt the assertion - if (config('samlidp.encrypt_assertion')) { + if ($this->encryptAssertion()) { $this->setSpCertificate(); $encryptedAssertion = new EncryptedAssertionWriter(); - $encryptedAssertion->encrypt($assertion, KeyHelper::createPublicKey( - (new X509Certificate)->loadPem($this->sp_certificate) - )); + $encryptedAssertion->encrypt( + $assertion, + KeyHelper::createPublicKey((new X509Certificate())->loadPem($this->sp_certificate)) + ); $this->response->addEncryptedAssertion($encryptedAssertion); } else { $this->response->addAssertion($assertion); } if (config('samlidp.messages_signed')) { - $this->response->setSignature(new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm)); + $this->response->setSignature( + new SignatureWriter($this->certificate, $this->private_key, $this->digest_algorithm) + ); } return $this->send(SamlConstants::BINDING_SAML2_HTTP_POST); @@ -146,9 +155,9 @@ public function response() */ public function send($binding_type) { - $bindingFactory = new BindingFactory; + $bindingFactory = new BindingFactory(); $postBinding = $bindingFactory->create($binding_type); - $messageContext = new MessageContext; + $messageContext = new MessageContext(); $messageContext->setMessage($this->response)->asResponse(); $message = $messageContext->getMessage(); $message->setRelayState(request('RelayState')); @@ -159,10 +168,7 @@ public function send($binding_type) private function setDestination() { - $destination = config(sprintf( - 'samlidp.sp.%s.destination', - $this->getServiceProvider($this->authn_request) - )); + $destination = config(sprintf('samlidp.sp.%s.destination', $this->getServiceProvider($this->authn_request))); if (empty($destination)) { throw new DestinationMissingException( @@ -175,11 +181,10 @@ private function setDestination() $queryParams = $this->getQueryParams(); if (!empty($queryParams)) { - if (!parse_url($destination, PHP_URL_QUERY)){ + if (!parse_url($destination, PHP_URL_QUERY)) { $destination = Str::finish(url($destination), '?') . Arr::query($queryParams); - } - else{ - $destination .= '&'.Arr::query($queryParams); + } else { + $destination .= '&' . Arr::query($queryParams); } } @@ -188,25 +193,39 @@ private function setDestination() private function getQueryParams() { - $queryParams = config(sprintf( - 'samlidp.sp.%s.query_params', - $this->getServiceProvider($this->authn_request) - )); + $queryParams = config(sprintf('samlidp.sp.%s.query_params', $this->getServiceProvider($this->authn_request))); if (is_null($queryParams)) { $queryParams = [ - 'idp' => config('app.url') + 'idp' => config('app.url'), ]; } return $queryParams; } - public function setSpCertificate() + /** + * @return void + */ + private function setSpCertificate() + { + $this->sp_certificate = config( + sprintf('samlidp.sp.%s.certificate', $this->getServiceProvider($this->authn_request)) + ); + } + + /** + * Check to see if the SP wants to encrypt assertions first + * If its not set, default to base encryption assertion config + * Otherwise return true + * + * @return boolean + */ + private function encryptAssertion(): bool { - $this->sp_certificate = config(sprintf( - 'samlidp.sp.%s.certificate', - $this->getServiceProvider($this->authn_request) - )); + return config( + sprintf('samlidp.sp.%s.encrypt_assertion', $this->getServiceProvider($this->authn_request)), + config('samlidp.encrypt_assertion', true) + ); } }