The SP can verify the signature with the copy of the certificate it
holds. Is that all that's needed to prevent forgery of a SAMLResponse
hitting the ACS?
No. Replay prevention is a big part of this (more on that in a minute). In addition, determining what is signed (the assertion? the entire response?) has bearing, and then there's the decision as to whether or not any of the attributes being shared from the IdP need to be encrypted (or maybe the whole assertion does). Generally, I see administrators choosing to sign the response, and as long as the IdP isn't passing what it considers PII, then encryption won't be needed.
Or will the SP need to make further checks, perhaps with a private key
The only time that the SP's private key comes into play is if the SP is signing
AuthnRequests (which, if you are supporting SP-init, is a good thing), or if the IdP wants to send you encrypted assertions or attributes. The encryption of attributes requires it to be done with the SP's public key at the IdP, so the SP can decrypt with its private key.
Additionally, I've read replay attacks need to be protected from, but
NotOnOrAfter exists surely that means a response is valid until
then so why would we protected from replays if the whole idea is that
a response should stay working up to some specified date?
This probably is more of a thought experiment. What is the mechanism being used to transport the SAML response? The browser, right? How secure is that? If the browser is compromised (or otherwise MITM'd), and an attacker can get the SAML response, then without the Replay Prevention the attacker can provide the response to your SP and authenticate as the legitimate user. The length of time that Replay Prevention must be in place is the length of the session granted by the response (the period between
NotOnOrAfter). It should be noted that some service providers (don't become one of these) drive the user's session time based on those two times. That's silly. These values should be used to manage the validity period of the assertion and set to as short a length as technically possible (thirty seconds should be enough). Speaking of short time frames, make sure your SP server time is correct - I can't tell you how many times I've found where neither the SP or IdP had their servers properly synching time to a central service. That makes putting time constraints on assertions really difficult if servers don't actually know what time it is.
Finally, I've read that I must consider recording a AuthnRequest ID so
that I can verify the IDP got it. The IDP can supply it in
InResponseTo of the SAMLResponse. Is this actually good?
There's good reason for doing SP-init - it prevents the possibility of data leakage of the resource the user is trying to get to. Consider the following... User shows up to a resource and they aren't authenticated. The resource says "hey pal, go get authenticated, and redirects them to the SP. The SP captures the referrer (which is the resource the user is trying to get to) and stores it temporarily, associating it with some immutable ID. The SP then sends the user to the IdP with an AuthnRequest that has that immutable ID. When the IdP sends the user back with that ID in the
InResponseTo, then a couple of cool things can happen - the SP can validate that the Response came from the proper IdP (very important if you are a service provider for multiple IdPs), and it can properly redirect the user to the resource that they wanted. In addition, since the it's an Opaque ID, the IdP doesn't have information about what the user was trying to access (which could somehow equate to PII, or have other privacy and security implications).